From d0419321a4282accc974b9e4167583e62b8dec73 Mon Sep 17 00:00:00 2001 From: Inside4ndroid Date: Sat, 16 May 2026 12:29:45 +0100 Subject: [PATCH] Huge update see changelog for details. --- CHANGELOG.md | 27 + README.md | 30 +- package-lock.json | 1393 ++++++++------------------------- package.json | 6 +- providers/MP4Hydra.js | 488 ------------ providers/Showbox.js | 14 +- providers/VidZee.js | 232 +++--- providers/dahmermovies.js | 152 ++++ providers/lordflix.js | 118 +++ providers/moviesmod.js | 105 ++- providers/notorrent.js | 92 +++ providers/registry.js | 42 +- providers/uhdmovies.js | 1418 ---------------------------------- providers/videasy.js | 116 +++ providers/vidlink.js | 51 ++ providers/vidsrcextractor.js | 411 ---------- providers/vixsrc.js | 344 ++++----- public/config.html | 6 +- public/index.js | 8 +- restart.trigger | 2 +- utils/auth-users.json | 2 +- utils/config.js | 2 +- utils/user-config.json | 27 +- 23 files changed, 1328 insertions(+), 3758 deletions(-) delete mode 100644 providers/MP4Hydra.js create mode 100644 providers/dahmermovies.js create mode 100644 providers/lordflix.js create mode 100644 providers/notorrent.js delete mode 100644 providers/uhdmovies.js create mode 100644 providers/videasy.js create mode 100644 providers/vidlink.js delete mode 100644 providers/vidsrcextractor.js diff --git a/CHANGELOG.md b/CHANGELOG.md index c57aa0a..565e457 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,33 @@ All notable changes to this project will be documented in this file. +## [1.1.0] - 2026-05-16 + +## Dependency Security +- **npm vulnerabilities fixed**: Upgraded `cheerio` (1.0.0 → 1.2.0) and `axios-cookiejar-support` (6.0.2 → 7.0.0) to eliminate 14 vulnerabilities (including `undici <=6.23.0` high/critical CVEs). Result: 0 vulnerabilities. + +### Added +- **Videasy provider**: 10-server parallel scraper via enc-dec.app decrypt relay. +- **Vidlink provider**: Single-stream HLS source via enc-dec.app encode relay. +- **LordFlix provider**: 9-server scraper via enc-dec.app (replaces defunct Vidsync). +- **NoTorrent provider**: Stremio addon API bridge (`addon-osvh.onrender.com`). +- **DahmerMovies provider**: Open-directory direct file link scraper with proxy rewrite. +- **GitHub Sponsors button**: Official iframe embed in admin sidebar and README badge. + +### Removed +- **Vidsync provider**: All 6 servers returned HTTP 401 — API locked. Replaced by LordFlix. +- **MP4Hydra provider**: Dead upstream, removed entirely. +- **UHDMovies provider**: Dead upstream, removed entirely. +- **vidsrcextractor.js**: Unused legacy file removed. + +### Fixed +- **Provider enable/disable not working at runtime**: Removed the startup-time enabled/disabled filter. All providers are now always loaded into the registry array. `listProviders()` and `getProvider()` now call `isProviderEnabled(name)` which reads the live `config` object on every call. Changes take effect immediately without a server restart. +- **Showbox/FebBox provider**: Restored with correct `responseType:'text'`, AJAX headers, and double `ui=` cookie bug fix. +- **MoviesMod provider**: Updated class-based selectors and added support for `links.modpro.blog`, `cloud.unblockedgames.world`, `tech.examdegree.site`, and `driveleech.net` domains. +- **VixSrc provider**: Corrected to proper 2-step API flow. + +--- + ## [1.0.9] - 2025-11-19 ### Removed diff --git a/README.md b/README.md index 0079507..6ec1940 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ - +

@@ -34,7 +34,7 @@ ## ✨ Features - **Multi‑TMDB Key Rotation** – Supply multiple API keys; one is chosen randomly per request. -- **Provider Aggregation** – Pluggable providers (4khdhub, MoviesMod, MP4Hydra, VidZee, Vixsrc, UHDMovies) with per‑provider enable toggles + default selection. +- **Provider Aggregation** – Pluggable providers (Showbox/FebBox, 4KHDHub, MoviesMod, VidZee, VixSrc, Videasy, Vidlink, LordFlix, NoTorrent, DahmerMovies) with per‑provider enable toggles + default selection. - **🔥 Plugin System** – Drop new provider files in `providers/` and add its exported function to the registry map (`providers/registry.js` → `providerFunctionMap`). - **Dynamic Filtering** – Minimum quality presets, custom JSON quality map, codec exclusion rules (presets + JSON). - **Runtime Overrides UI** – Fully interactive web admin at `/` (login protected) writing to `utils/user-config.json`. @@ -225,12 +225,16 @@ Session is revalidated on visibility and back/forward navigation to prevent stal The API supports a plugin system. Drop a new provider file in the `providers/` folder and register its exported function in `providers/registry.js` under `providerFunctionMap`. ### Current Built-in Providers +- `showbox` - Showbox/FebBox streams (requires FebBox JWT cookie) - `4khdhub` - 4KHDHub streams -- `moviesmod` - MoviesMod streams -- `mp4hydra` - MP4Hydra streams +- `moviesmod` - MoviesMod streams - `vidzee` - VidZee streams -- `vixsrc` - Vixsrc streams -- `uhdmovies` - UHD Movies streams +- `vixsrc` - VixSrc streams +- `videasy` - Videasy streams (10 servers via enc-dec.app) +- `vidlink` - Vidlink streams (via enc-dec.app) +- `lordflix` - LordFlix streams (9 servers via enc-dec.app) +- `notorrent` - NoTorrent streams (Stremio addon API) +- `dahmermovies` - DahmerMovies streams (direct file links) ### Adding a New Provider @@ -240,12 +244,16 @@ The API supports a plugin system. Drop a new provider file in the `providers/` f ```js // providers/registry.js const providerFunctionMap = { + 'Showbox.js': 'getStreamsFromTmdbId', '4khdhub.js': 'get4KHDHubStreams', 'moviesmod.js': 'getMoviesModStreams', - 'MP4Hydra.js': 'getMP4HydraStreams', 'VidZee.js': 'getVidZeeStreams', 'vixsrc.js': 'getVixsrcStreams', - 'uhdmovies.js': 'getUHDMoviesStreams', + 'videasy.js': 'getVideasyStreams', + 'vidlink.js': 'getVidlinkStreams', + 'lordflix.js': 'getLordflixStreams', + 'notorrent.js': 'getNotorrentStreams', + 'dahmermovies.js': 'getDahmermoviesStreams', 'yourprovider.js': 'getYourproviderStreams' }; ``` @@ -378,7 +386,11 @@ PRs welcome. Keep changes focused and avoid unrelated formatting churn. For new ## ❤️ Sponsorship If this project helps you, consider sponsoring to support continued development & maintenance: -➡️ **GitHub Sponsors:** https://github.com/sponsors/Inside4ndroid +

+ + Sponsor on GitHub + +

Every contribution accelerates feature delivery & sustainability. diff --git a/package-lock.json b/package-lock.json index d6531aa..0a8d4fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,18 @@ { "name": "tmdb-embed-api", - "version": "1.0.8", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tmdb-embed-api", - "version": "1.0.8", + "version": "1.1.0", "dependencies": { "aromanize": "^0.1.5", "axios": "^1.9.0", - "axios-cookiejar-support": "^6.0.2", + "axios-cookiejar-support": "^7.0.0", "body-parser": "^2.2.0", - "cheerio": "^1.0.0", + "cheerio": "^1.2.0", "cors": "^2.8.5", "crypto-js": "^4.2.0", "dotenv": "^16.5.0", @@ -22,11 +22,9 @@ "jsdom": "^26.1.0", "node-fetch": "^2.7.0", "p-limit": "^6.2.0", - "puppeteer": "^24.11.0", "string-similarity": "^4.0.4", "tough-cookie": "^5.1.2", "ttl-cache": "^1.0.2", - "vm2": "^3.9.19", "yargs": "^17.7.2" }, "devDependencies": { @@ -56,29 +54,6 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@csstools/color-helpers": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", @@ -163,6 +138,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -185,6 +161,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -378,88 +355,6 @@ "node": ">= 8" } }, - "node_modules/@puppeteer/browsers": { - "version": "2.10.5", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.5.tgz", - "integrity": "sha512-eifa0o+i8dERnngJwKrfp3dEq7ia5XFyoqB17S4gK8GhsQE4/P8nxOfQSE0zQHxzzLo/cmF+7+ywEQ7wK7Fb+w==", - "license": "Apache-2.0", - "dependencies": { - "debug": "^4.4.1", - "extract-zip": "^2.0.1", - "progress": "^2.0.3", - "proxy-agent": "^6.5.0", - "semver": "^7.7.2", - "tar-fs": "^3.0.8", - "yargs": "^17.7.2" - }, - "bin": { - "browsers": "lib/cjs/main-cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@puppeteer/browsers/node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@puppeteer/browsers/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==", - "license": "MIT" - }, - "node_modules/@puppeteer/browsers/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "24.0.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.4.tgz", - "integrity": "sha512-ulyqAkrhnuNq9pB76DRBTkcS6YsmDALy6Ua63V8OhrOBgbcYt6IOdzpw5P1+dyRIyMerzLkeYWBeOXPpA9GMAA==", - "license": "MIT", - "optional": true, - "dependencies": { - "undici-types": "~7.8.0" - } - }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -484,7 +379,9 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -502,18 +399,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/agent-base": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", @@ -524,9 +409,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -582,6 +467,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, "license": "Python-2.0" }, "node_modules/aromanize": { @@ -596,24 +482,6 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, - "node_modules/ast-types": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", - "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/ast-types/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -621,26 +489,27 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", - "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz", + "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" } }, "node_modules/axios-cookiejar-support": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/axios-cookiejar-support/-/axios-cookiejar-support-6.0.2.tgz", - "integrity": "sha512-UO/g6DKfVoxnZkZz1NN669bMDjGV3snZnAZGZqIwEd8FdvFI17/rXLyMBm1j1cgtb2O6Jyi4MJ7ll49NPBEMNg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/axios-cookiejar-support/-/axios-cookiejar-support-7.0.0.tgz", + "integrity": "sha512-IpxfiGiEsHp3uj3fjtTiM2LjQGKF5Ig7gZSsVKTJ9RN6KZor17bEwU1ZgYMcrrT3qKP3pz1FdzufgHjtDxKIvg==", "license": "MIT", "dependencies": { - "http-cookie-agent": "^7.0.1" + "http-cookie-agent": "^8.0.0" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.0.0" }, "funding": { "url": "https://github.com/sponsors/3846masa" @@ -650,46 +519,53 @@ "tough-cookie": ">=4.0.0" } }, - "node_modules/axios-cookiejar-support/node_modules/http-cookie-agent": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/http-cookie-agent/-/http-cookie-agent-7.0.1.tgz", - "integrity": "sha512-lZHFZUdPTw64PdksQac5xbUd4NWjUbyDYnvR//2sbLpcC4UqEUW0x/6O+rDntVzJzJ07QvhtL5XZSC+c5EK+IQ==", + "node_modules/axios/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "license": "MIT", "dependencies": { - "agent-base": "^7.1.3" + "debug": "4" }, "engines": { - "node": ">=20.0.0" + "node": ">= 6.0.0" + } + }, + "node_modules/axios/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" }, - "funding": { - "url": "https://github.com/sponsors/3846masa" - }, - "peerDependencies": { - "tough-cookie": "^4.0.0 || ^5.0.0", - "undici": "^7.0.0" + "engines": { + "node": ">=6.0" }, "peerDependenciesMeta": { - "undici": { + "supports-color": { "optional": true } } }, - "node_modules/axios-cookiejar-support/node_modules/undici": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", - "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", + "node_modules/axios/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "license": "MIT", - "optional": true, - "peer": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, "engines": { - "node": ">=20.18.1" + "node": ">= 6" } }, - "node_modules/b4a": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", - "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", - "license": "Apache-2.0" + "node_modules/axios/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==", + "license": "MIT" }, "node_modules/balanced-match": { "version": "1.0.2", @@ -698,87 +574,6 @@ "dev": true, "license": "MIT" }, - "node_modules/bare-events": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz", - "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==", - "license": "Apache-2.0", - "optional": true - }, - "node_modules/bare-fs": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.5.tgz", - "integrity": "sha512-1zccWBMypln0jEE05LzZt+V/8y8AQsQQqxtklqaIyg5nu6OAYFhZxPXinJTSG+kU5qyNmeLgcn9AW7eHiCHVLA==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "bare-events": "^2.5.4", - "bare-path": "^3.0.0", - "bare-stream": "^2.6.4" - }, - "engines": { - "bare": ">=1.16.0" - }, - "peerDependencies": { - "bare-buffer": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - } - } - }, - "node_modules/bare-os": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz", - "integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==", - "license": "Apache-2.0", - "optional": true, - "engines": { - "bare": ">=1.14.0" - } - }, - "node_modules/bare-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", - "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "bare-os": "^3.0.1" - } - }, - "node_modules/bare-stream": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz", - "integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "streamx": "^2.21.0" - }, - "peerDependencies": { - "bare-buffer": "*", - "bare-events": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "node_modules/basic-ftp": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", - "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -793,29 +588,33 @@ } }, "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", - "debug": "^4.4.0", + "debug": "^4.4.3", "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", + "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/body-parser/node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -829,6 +628,22 @@ } } }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/body-parser/node_modules/media-typer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", @@ -865,21 +680,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/body-parser/node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/body-parser/node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", @@ -901,9 +701,9 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -924,15 +724,6 @@ "node": ">=8" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -975,6 +766,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -1021,25 +813,25 @@ } }, "node_modules/cheerio": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", - "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", "license": "MIT", "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", - "domutils": "^3.1.0", - "encoding-sniffer": "^0.2.0", - "htmlparser2": "^9.1.0", - "parse5": "^7.1.2", - "parse5-htmlparser2-tree-adapter": "^7.0.0", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", - "undici": "^6.19.5", + "undici": "^7.19.0", "whatwg-mimetype": "^4.0.0" }, "engines": { - "node": ">=18.17" + "node": ">=20.18.1" }, "funding": { "url": "https://github.com/cheeriojs/cheerio?sponsor=1" @@ -1087,19 +879,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/chromium-bidi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-5.1.0.tgz", - "integrity": "sha512-9MSRhWRVoRPDG0TgzkHrshFSJJNZzfY5UFqUMuksg7zL1yoZIZ3jLB0YAgHclbiAxPI86pBnwDX1tbzoiV8aFw==", - "license": "Apache-2.0", - "dependencies": { - "mitt": "^3.0.1", - "zod": "^3.24.1" - }, - "peerDependencies": { - "devtools-protocol": "*" - } - }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -1223,32 +1002,6 @@ "node": ">= 0.10" } }, - "node_modules/cosmiconfig": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", - "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1311,15 +1064,6 @@ "node": ">=18" } }, - "node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, "node_modules/data-urls": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", @@ -1389,20 +1133,6 @@ "dev": true, "license": "MIT" }, - "node_modules/degenerator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", - "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", - "license": "MIT", - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1431,12 +1161,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/devtools-protocol": { - "version": "0.0.1464554", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1464554.tgz", - "integrity": "sha512-CAoP3lYfwAGQTaAXYvA6JZR0fjGUb7qec1qf4mToyoH2TZgUFeIqYcjh6f9jNuhHfuZiEdH+PONHYrLhRQX6aw==", - "license": "BSD-3-Clause" - }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -1553,9 +1277,9 @@ } }, "node_modules/encoding-sniffer": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", - "integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", "license": "MIT", "dependencies": { "iconv-lite": "^0.6.3", @@ -1565,15 +1289,6 @@ "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" } }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -1586,24 +1301,6 @@ "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", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.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", @@ -1677,27 +1374,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "license": "BSD-2-Clause", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, "node_modules/eslint": { "version": "8.57.1", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", @@ -1705,6 +1381,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -1841,19 +1518,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/esquery": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", @@ -1884,6 +1548,7 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -1893,6 +1558,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -1908,39 +1574,39 @@ } }, "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", + "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.13.0", + "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", + "send": "~0.19.0", + "serve-static": "~1.16.2", "setprototypeof": "1.2.0", - "statuses": "2.0.1", + "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" @@ -1954,29 +1620,49 @@ } }, "node_modules/express/node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", + "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", "type-is": "~1.6.18", - "unpipe": "1.0.0" + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/express/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/express/node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -1990,63 +1676,29 @@ } }, "node_modules/express/node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8" } }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, - "node_modules/extract-zip/node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "node_modules/express/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">= 0.8" } }, - "node_modules/extract-zip/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==", - "license": "MIT" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2054,12 +1706,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "license": "MIT" - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -2084,15 +1730,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -2170,16 +1807,16 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", @@ -2197,9 +1834,9 @@ } }, "node_modules/form-data": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", - "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -2307,58 +1944,6 @@ "node": ">= 0.4" } }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-uri": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz", - "integrity": "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==", - "license": "MIT", - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/get-uri/node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/get-uri/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==", - "license": "MIT" - }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -2497,9 +2082,9 @@ } }, "node_modules/htmlparser2": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", - "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", { @@ -2511,8 +2096,53 @@ "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", - "domutils": "^3.1.0", - "entities": "^4.5.0" + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/http-cookie-agent": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/http-cookie-agent/-/http-cookie-agent-8.0.0.tgz", + "integrity": "sha512-c5A2W4NXYRQR+ZOEA+nzLlWj0K7zXAG21UZaDKfZrFSlmmb1MMXBVDhQJNUNYkYJZwFi0ptlNu4r37tEM45tvg==", + "license": "MIT", + "dependencies": { + "agent-base": "^9.0.0" + }, + "engines": { + "node": ">=22.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/3846masa" + }, + "peerDependencies": { + "tough-cookie": "^4.0.0 || ^5.0.0 || ^6.0.0", + "undici": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "undici": { + "optional": true + } + } + }, + "node_modules/http-cookie-agent/node_modules/agent-base": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz", + "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==", + "license": "MIT", + "engines": { + "node": ">= 20" } }, "node_modules/http-errors": { @@ -2636,6 +2266,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -2676,19 +2307,6 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, - "node_modules/ip-address": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", - "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", - "license": "MIT", - "dependencies": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" - }, - "engines": { - "node": ">= 12" - } - }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -2698,12 +2316,6 @@ "node": ">= 0.10" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "license": "MIT" - }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -2773,16 +2385,11 @@ "dev": true, "license": "ISC" }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -2791,12 +2398,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsbn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", - "license": "MIT" - }, "node_modules/jsdom": { "version": "26.1.0", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", @@ -2877,12 +2478,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "license": "MIT" - }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -2921,12 +2516,6 @@ "node": ">= 0.8.0" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "license": "MIT" - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2950,15 +2539,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -3029,9 +2609,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -3041,12 +2621,6 @@ "node": "*" } }, - "node_modules/mitt": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", - "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", - "license": "MIT" - }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -3069,15 +2643,6 @@ "node": ">= 0.6" } }, - "node_modules/netmask": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", - "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -3230,6 +2795,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -3313,65 +2879,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/pac-proxy-agent": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", - "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", - "license": "MIT", - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.6", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-proxy-agent/node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/pac-proxy-agent/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==", - "license": "MIT" - }, - "node_modules/pac-resolver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", - "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", - "license": "MIT", - "dependencies": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -3380,24 +2892,6 @@ "node": ">=6" } }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/parse5": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", @@ -3487,27 +2981,15 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", "license": "MIT" }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -3527,15 +3009,6 @@ "node": ">= 0.8.0" } }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -3549,53 +3022,14 @@ "node": ">= 0.10" } }, - "node_modules/proxy-agent": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", - "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.6", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.1.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-agent/node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/proxy-agent/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==", - "license": "MIT" - }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/pstree.remy": { "version": "1.1.8", @@ -3604,16 +3038,6 @@ "dev": true, "license": "MIT" }, - "node_modules/pump": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", - "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3623,74 +3047,13 @@ "node": ">=6" } }, - "node_modules/puppeteer": { - "version": "24.11.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.11.0.tgz", - "integrity": "sha512-7XFpexeYIMen/pfGiDjUpIV7BB9EuVQBtXeQ0cUOmVJTkAuQD3oPHVU7iYeA/mrIJyk79sfOmT20a6SEDwGTLQ==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@puppeteer/browsers": "2.10.5", - "chromium-bidi": "5.1.0", - "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1464554", - "puppeteer-core": "24.11.0", - "typed-query-selector": "^2.12.0" - }, - "bin": { - "puppeteer": "lib/cjs/puppeteer/node/cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/puppeteer-core": { - "version": "24.11.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.11.0.tgz", - "integrity": "sha512-ncLty0wRjCX67UxzXemmD1mOxb6y1Xzrx1nym8SAQ6cYrcypOVf77CfcZru6P+EiMA9gNDeQNscowKSE9xvhMw==", - "license": "Apache-2.0", - "dependencies": { - "@puppeteer/browsers": "2.10.5", - "chromium-bidi": "5.1.0", - "debug": "^4.4.1", - "devtools-protocol": "0.0.1464554", - "typed-query-selector": "^2.12.0", - "ws": "^8.18.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/puppeteer-core/node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/puppeteer-core/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==", - "license": "MIT" - }, "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -3730,16 +3093,61 @@ } }, "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.6.3", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -3770,6 +3178,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -3974,13 +3383,13 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -4052,83 +3461,6 @@ "node": ">=10" } }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.5.tgz", - "integrity": "sha512-iF+tNDQla22geJdTyJB1wM/qrX9DMRwWrciEPwWLPRWAUEM8sQiyxgckLxWT1f7+9VabJS0jTGGr4QgBuvi6Ww==", - "license": "MIT", - "dependencies": { - "ip-address": "^9.0.5", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/socks-proxy-agent/node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socks-proxy-agent/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==", - "license": "MIT" - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "license": "BSD-3-Clause" - }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -4138,19 +3470,6 @@ "node": ">= 0.8" } }, - "node_modules/streamx": { - "version": "2.22.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz", - "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==", - "license": "MIT", - "dependencies": { - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" - }, - "optionalDependencies": { - "bare-events": "^2.2.0" - } - }, "node_modules/string-similarity": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/string-similarity/-/string-similarity-4.0.4.tgz", @@ -4202,40 +3521,6 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "license": "MIT" }, - "node_modules/tar-fs": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.10.tgz", - "integrity": "sha512-C1SwlQGNLe/jPNqapK8epDsXME7CAJR5RL3GcE6KWx1d9OUByzoHVcbu1VPI8tevg9H8Alae0AApHHFGzrD5zA==", - "license": "MIT", - "dependencies": { - "pump": "^3.0.0", - "tar-stream": "^3.1.5" - }, - "optionalDependencies": { - "bare-fs": "^4.0.1", - "bare-path": "^3.0.0" - } - }, - "node_modules/tar-stream": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", - "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", - "license": "MIT", - "dependencies": { - "b4a": "^1.6.4", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/text-decoder": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", - "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", - "license": "Apache-2.0", - "dependencies": { - "b4a": "^1.6.4" - } - }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -4298,6 +3583,7 @@ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", "license": "BSD-3-Clause", + "peer": true, "dependencies": { "tldts": "^6.1.32" }, @@ -4356,12 +3642,6 @@ "node": ">= 0.6" } }, - "node_modules/typed-query-selector": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", - "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", - "license": "MIT" - }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -4370,21 +3650,15 @@ "license": "MIT" }, "node_modules/undici": { - "version": "6.21.3", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", - "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", + "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", "license": "MIT", + "peer": true, "engines": { - "node": ">=18.17" + "node": ">=20.18.1" } }, - "node_modules/undici-types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", - "license": "MIT", - "optional": true - }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -4422,23 +3696,6 @@ "node": ">= 0.8" } }, - "node_modules/vm2": { - "version": "3.9.19", - "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.19.tgz", - "integrity": "sha512-J637XF0DHDMV57R6JyVsTak7nIL8gy5KH4r1HiwWLf/4GBbb5MKL5y7LpmF4A8E2nR6XmzpmMFQ7V7ppPTmUQg==", - "deprecated": "The library contains critical security issues and should not be used for production! The maintenance of the project has been discontinued. Consider migrating your code to isolated-vm.", - "license": "MIT", - "dependencies": { - "acorn": "^8.7.0", - "acorn-walk": "^8.2.0" - }, - "bin": { - "vm2": "bin/vm2" - }, - "engines": { - "node": ">=6.0" - } - }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", @@ -4558,6 +3815,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, "license": "ISC" }, "node_modules/ws": { @@ -4655,16 +3913,6 @@ "node": ">=8" } }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, "node_modules/yocto-queue": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", @@ -4676,15 +3924,6 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } - }, - "node_modules/zod": { - "version": "3.25.67", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", - "integrity": "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } } } } diff --git a/package.json b/package.json index 59534ca..06e4a08 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tmdb-embed-api", - "version": "1.0.9", + "version": "1.1.0", "description": "Standalone streaming embed source aggregation API (TMDB ID in, streams out)", "main": "apiServer.js", "scripts": { @@ -11,9 +11,9 @@ "dependencies": { "aromanize": "^0.1.5", "axios": "^1.9.0", - "axios-cookiejar-support": "^6.0.2", + "axios-cookiejar-support": "^7.0.0", "body-parser": "^2.2.0", - "cheerio": "^1.0.0", + "cheerio": "^1.2.0", "cors": "^2.8.5", "crypto-js": "^4.2.0", "dotenv": "^16.5.0", diff --git a/providers/MP4Hydra.js b/providers/MP4Hydra.js deleted file mode 100644 index 080867e..0000000 --- a/providers/MP4Hydra.js +++ /dev/null @@ -1,488 +0,0 @@ -const axios = require('axios'); -const FormData = require('form-data'); - -// Helper function to generate slug from title -function generateSlug(title) { - return title.toLowerCase() - .replace(/[^\w\s-]/g, '') // Remove special characters - .replace(/\s+/g, '-') // Replace spaces with hyphens - .replace(/-+/g, '-'); // Replace multiple hyphens with a single hyphen -} - -// Process episode data from MP4Hydra response -function processEpisode(episode, baseServer, serverName, serverNumber) { - const videoUrl = `${baseServer}${episode.src}`; - const subtitles = episode.subs ? episode.subs.map(sub => ({ - label: sub.label, - url: `${baseServer}${sub.src}` - })) : []; - - return { - title: episode.show_title || episode.title, - episode: episode.title, - type: episode.type, - quality: episode.quality || episode.label, - videoUrl: videoUrl, - server: serverName, - serverNumber: serverNumber, - subtitles: subtitles - }; -} - -// Main function to get streams from MP4Hydra -async function getMP4HydraStreams(tmdbId, mediaType, seasonNum = null, episodeNum = null) { - try { - console.log(`[MP4Hydra] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}, Season: ${seasonNum}, Episode: ${episodeNum}`); - - // Get TMDB details - const details = await getTMDBDetails(tmdbId, mediaType); - if (!details) { - console.log(`[MP4Hydra] Could not fetch details for TMDB ID: ${tmdbId}`); - return []; - } - - console.log(`[MP4Hydra] Found title: ${details.title} (${details.year})`); - - // Generate slug in the format "movie-name-year" - let slug = details.slug; - if (mediaType === 'movie' && details.year) { - slug = `${details.slug}-${details.year}`; - } - console.log(`[MP4Hydra] Using slug: ${slug}`); - - // Create form data for multipart/form-data request - const formData = new FormData(); - formData.append('v', '8'); - formData.append('z', JSON.stringify([{ - s: slug, - t: mediaType, - se: seasonNum, - ep: episodeNum - }])); - - // Make request to MP4Hydra API - const response = await axios({ - method: 'post', - url: 'https://mp4hydra.org/info2?v=8', - data: formData, - headers: { - ...formData.getHeaders(), - 'User-Agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Mobile Safari/537.36', - 'Accept': '*/*', - 'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8', - 'Origin': 'https://mp4hydra.org', - 'Referer': `https://mp4hydra.org/${mediaType}/${slug}`, - 'Sec-Fetch-Dest': 'empty', - 'Sec-Fetch-Mode': 'cors', - 'Sec-Fetch-Site': 'same-origin' - }, - timeout: 10000 - }); - - // Process response - if (response.data && response.data.playlist && response.data.playlist.length > 0) { - const playlist = response.data.playlist; - const servers = response.data.servers; - - console.log(`[MP4Hydra] Found ${playlist.length} videos`); - console.log(`[MP4Hydra] Available servers: ${Object.keys(servers).join(', ')}`); - - // For TV shows, find the specific episode - if (mediaType === 'tv' && seasonNum && episodeNum) { - const paddedSeason = seasonNum.toString().padStart(2, '0'); - const paddedEpisode = episodeNum.toString().padStart(2, '0'); - const seasonEpisode = `S${paddedSeason}E${paddedEpisode}`; - - const targetEpisode = playlist.find(item => - item.title && item.title.toUpperCase() === seasonEpisode.toUpperCase() - ); - - if (!targetEpisode) { - console.log(`[MP4Hydra] Could not find ${seasonEpisode}`); - return []; - } - - console.log(`[MP4Hydra] Found episode: ${targetEpisode.show_title || targetEpisode.title}`); - - // Process streams from main servers - const streams = []; - const serverConfig = [ - { name: 'Beta', number: '#1' }, - { name: 'Beta#3', number: '#2' } - ]; - - serverConfig.forEach(server => { - const serverName = server.name; - const serverNumber = server.number; - - if (servers[serverName]) { - const baseServer = servers[serverName]; - console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`); - - const processedEpisode = processEpisode(targetEpisode, baseServer, serverName, serverNumber); - - // Convert to standard stream format - streams.push({ - title: `${details.title} - ${seasonEpisode} - ${processedEpisode.quality} [MP4Hydra ${serverNumber}]`, - url: processedEpisode.videoUrl, - quality: processedEpisode.quality, - provider: "mp4hydra", - headers: { - 'Referer': 'https://mp4hydra.org/' - } - - }); - - // Add subtitle tracks if available - if (processedEpisode.subtitles && processedEpisode.subtitles.length > 0) { - streams[streams.length - 1].subtitles = processedEpisode.subtitles.map(sub => ({ - url: sub.url, - lang: sub.label - })); - } - } - }); - - return streams; - } - - // For movies, process all videos - const streams = []; - const serverConfig = [ - { name: 'Beta', number: '#1' }, - { name: 'Beta#3', number: '#2' } - ]; - - serverConfig.forEach(server => { - const serverName = server.name; - const serverNumber = server.number; - - if (servers[serverName]) { - const baseServer = servers[serverName]; - console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`); - - playlist.forEach(item => { - const processedItem = processEpisode(item, baseServer, serverName, serverNumber); - - // Convert to standard stream format - streams.push({ - title: `${details.title} - ${processedItem.quality} [MP4Hydra ${serverNumber}]`, - url: processedItem.videoUrl, - quality: processedItem.quality, - provider: "mp4hydra", - headers: { - 'Referer': 'https://mp4hydra.org/' - } - - }); - - // Add subtitle tracks if available - if (processedItem.subtitles && processedItem.subtitles.length > 0) { - streams[streams.length - 1].subtitles = processedItem.subtitles.map(sub => ({ - url: sub.url, - lang: sub.label - })); - } - }); - } - }); - - return streams; - } else { - // Try with original title if the first attempt failed - if (details.title !== details.original_title) { - return await tryAlternativeTitle(details, mediaType, seasonNum, episodeNum); - } - - // Try without year for movies - if (mediaType === 'movie' && details.year) { - return await tryWithoutYear(details, mediaType, seasonNum, episodeNum); - } - - console.log('[MP4Hydra] No streaming links found'); - return []; - } - } catch (error) { - console.error(`[MP4Hydra] Error fetching streams:`, error.message); - return []; - } -} - -// Helper function to try with original title -async function tryAlternativeTitle(details, mediaType, seasonNum, episodeNum) { - try { - console.log(`[MP4Hydra] Retrying with original title: ${details.original_title}`); - const originalSlug = generateSlug(details.original_title); - let originalFullSlug = originalSlug; - - if (mediaType === 'movie' && details.year) { - originalFullSlug = `${originalSlug}-${details.year}`; - } - - // Create new form data with original title - const formData = new FormData(); - formData.append('v', '8'); - formData.append('z', JSON.stringify([{ - s: originalFullSlug, - t: mediaType, - se: seasonNum, - ep: episodeNum - }])); - - const response = await axios({ - method: 'post', - url: 'https://mp4hydra.org/info2?v=8', - data: formData, - headers: { - ...formData.getHeaders(), - 'User-Agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Mobile Safari/537.36', - 'Accept': '*/*', - 'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8', - 'Origin': 'https://mp4hydra.org', - 'Referer': `https://mp4hydra.org/${mediaType}/${originalFullSlug}`, - 'Sec-Fetch-Dest': 'empty', - 'Sec-Fetch-Mode': 'cors', - 'Sec-Fetch-Site': 'same-origin' - }, - timeout: 10000 - }); - - // Process response - if (response.data && response.data.playlist && response.data.playlist.length > 0) { - const playlist = response.data.playlist; - const servers = response.data.servers; - - console.log(`[MP4Hydra] Found ${playlist.length} videos with original title`); - - // For TV shows, find the specific episode - if (mediaType === 'tv' && seasonNum && episodeNum) { - const paddedSeason = seasonNum.toString().padStart(2, '0'); - const paddedEpisode = episodeNum.toString().padStart(2, '0'); - const seasonEpisode = `S${paddedSeason}E${paddedEpisode}`; - - const targetEpisode = playlist.find(item => - item.title && item.title.toUpperCase() === seasonEpisode.toUpperCase() - ); - - if (!targetEpisode) { - console.log(`[MP4Hydra] Could not find ${seasonEpisode} with original title`); - return []; - } - - // Process streams from main servers - const streams = []; - const serverConfig = [ - { name: 'Beta', number: '#1' }, - { name: 'Beta#3', number: '#2' } - ]; - - serverConfig.forEach(server => { - const serverName = server.name; - const serverNumber = server.number; - - if (servers[serverName]) { - const baseServer = servers[serverName]; - console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`); - - const processedEpisode = processEpisode(targetEpisode, baseServer, serverName, serverNumber); - - // Convert to standard stream format - streams.push({ - title: `${details.original_title} - ${seasonEpisode} - ${processedEpisode.quality} [MP4Hydra ${serverNumber}]`, - url: processedEpisode.videoUrl, - quality: processedEpisode.quality, - provider: "mp4hydra", - headers: { - 'Referer': 'https://mp4hydra.org/' - } - - }); - - // Add subtitle tracks if available - if (processedEpisode.subtitles && processedEpisode.subtitles.length > 0) { - streams[streams.length - 1].subtitles = processedEpisode.subtitles.map(sub => ({ - url: sub.url, - lang: sub.label - })); - } - } - }); - - return streams; - } - - // For movies, process all videos - const streams = []; - const serverConfig = [ - { name: 'Beta', number: '#1' }, - { name: 'Beta#3', number: '#2' } - ]; - - serverConfig.forEach(server => { - const serverName = server.name; - const serverNumber = server.number; - - if (servers[serverName]) { - const baseServer = servers[serverName]; - console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`); - - playlist.forEach(item => { - const processedItem = processEpisode(item, baseServer, serverName, serverNumber); - - // Convert to standard stream format - streams.push({ - name: `${details.original_title}`, - title: `${details.original_title} - ${processedItem.quality} [MP4Hydra ${serverNumber}]`, - url: processedItem.videoUrl, - quality: processedItem.quality - }); - - // Add subtitle tracks if available - if (processedItem.subtitles && processedItem.subtitles.length > 0) { - streams[streams.length - 1].subtitles = processedItem.subtitles.map(sub => ({ - url: sub.url, - lang: sub.label - })); - } - }); - } - }); - - return streams; - } - - return []; - } catch (error) { - console.error(`[MP4Hydra] Error with alternative title:`, error.message); - return []; - } -} - -// Helper function to try without year -async function tryWithoutYear(details, mediaType, seasonNum, episodeNum) { - try { - console.log(`[MP4Hydra] Retrying with title only (without year): ${details.title}`); - const titleOnlySlug = details.slug; - - // Create new form data with title only - const formData = new FormData(); - formData.append('v', '8'); - formData.append('z', JSON.stringify([{ - s: titleOnlySlug, - t: mediaType, - se: seasonNum, - ep: episodeNum - }])); - - const response = await axios({ - method: 'post', - url: 'https://mp4hydra.org/info2?v=8', - data: formData, - headers: { - ...formData.getHeaders(), - 'User-Agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Mobile Safari/537.36', - 'Accept': '*/*', - 'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8', - 'Origin': 'https://mp4hydra.org', - 'Referer': `https://mp4hydra.org/${mediaType}/${titleOnlySlug}`, - 'Sec-Fetch-Dest': 'empty', - 'Sec-Fetch-Mode': 'cors', - 'Sec-Fetch-Site': 'same-origin' - }, - timeout: 10000 - }); - - // Process response - if (response.data && response.data.playlist && response.data.playlist.length > 0) { - const playlist = response.data.playlist; - const servers = response.data.servers; - - console.log(`[MP4Hydra] Found ${playlist.length} videos with title only`); - - // For movies, process all videos - const streams = []; - const serverConfig = [ - { name: 'Beta', number: '#1' }, - { name: 'Beta#3', number: '#2' } - ]; - - serverConfig.forEach(server => { - const serverName = server.name; - const serverNumber = server.number; - - if (servers[serverName]) { - const baseServer = servers[serverName]; - console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`); - - playlist.forEach(item => { - const processedItem = processEpisode(item, baseServer, serverName, serverNumber); - - // Convert to standard stream format - streams.push({ - name: `${details.title}`, - title: `${details.title} - ${processedItem.quality} [MP4Hydra ${serverNumber}]`, - url: processedItem.videoUrl, - quality: processedItem.quality, - provider: "mp4hydra", - headers: { - 'Referer': 'https://mp4hydra.org/' - } - - }); - - // Add subtitle tracks if available - if (processedItem.subtitles && processedItem.subtitles.length > 0) { - streams[streams.length - 1].subtitles = processedItem.subtitles.map(sub => ({ - url: sub.url, - lang: sub.label - })); - } - }); - } - }); - - return streams; - } - - return []; - } catch (error) { - console.error(`[MP4Hydra] Error with title-only:`, error.message); - return []; - } -} - -// Helper function to get TMDB details -async function getTMDBDetails(tmdbId, mediaType) { - const { getTmdbApiKey } = require('../utils/tmdbKey'); - const TMDB_API_KEY = getTmdbApiKey(); - if (!TMDB_API_KEY) { - console.warn('[MP4Hydra] TMDB API key missing; skipping TMDB metadata fetch'); - return null; - } - - try { - console.log(`[MP4Hydra] Fetching ${mediaType} details for TMDB ID: ${tmdbId}`); - const response = await axios.get(`https://api.themoviedb.org/3/${mediaType}/${tmdbId}?api_key=${TMDB_API_KEY}`); - - if (mediaType === 'movie') { - return { - title: response.data.title, - original_title: response.data.original_title, - year: response.data.release_date ? response.data.release_date.split('-')[0] : null, - slug: generateSlug(response.data.title) - }; - } else { - return { - title: response.data.name, - original_title: response.data.original_name, - year: response.data.first_air_date ? response.data.first_air_date.split('-')[0] : null, - slug: generateSlug(response.data.name) - }; - } - } catch (error) { - console.error(`[MP4Hydra] Error fetching details from TMDB:`, error.message); - return null; - } -} - -module.exports = { getMP4HydraStreams }; \ No newline at end of file diff --git a/providers/Showbox.js b/providers/Showbox.js index 5a024c9..5a20163 100644 --- a/providers/Showbox.js +++ b/providers/Showbox.js @@ -62,8 +62,9 @@ const getCookieForRequest = async (regionPreference = null, userCookie = null) = try { const array = Array.isArray(global.currentRequestConfig.cookies) ? global.currentRequestConfig.cookies.filter(Boolean) : []; if (array.length > 0) { - // Just pick first for now (quota logic removed here if previously elsewhere) - baseCookieToUse = array[0]; + // Strip 'ui=' prefix if present — callers always prepend 'ui=' themselves + const raw = array[0]; + baseCookieToUse = raw.startsWith('ui=') ? raw.slice(3) : raw; } } catch (e) { console.warn(`[CookieManager] Cookie array selection error: ${e.message}`); @@ -1211,7 +1212,12 @@ const fetchSourcesForSingleFid = async (fidToProcess, shareKey, regionPreference const baseHeaders = { 'Cookie': `ui=${cookieForRequest}`, - 'Content-Type': 'application/x-www-form-urlencoded' + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-Requested-With': 'XMLHttpRequest', + 'Origin': 'https://www.febbox.com', + 'Referer': `https://www.febbox.com/share/${shareKey}`, + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', + 'Accept': '*/*', }; const finalPostUrl = FEBBOX_PLAYER_URL; @@ -1221,7 +1227,7 @@ const fetchSourcesForSingleFid = async (fidToProcess, shareKey, regionPreference console.log(` Fetching fresh player data for video FID: ${fidToProcess} (Share: ${shareKey}) directly to ${FEBBOX_PLAYER_URL}`); try { - const response = await axios.post(finalPostUrl, postDataForAxios, axiosConfig); + const response = await axios.post(finalPostUrl, postDataForAxios, { ...axiosConfig, responseType: 'text' }); const playerContent = response.data; // Mark the region as available if the request succeeded diff --git a/providers/VidZee.js b/providers/VidZee.js index 2d23524..5909df7 100644 --- a/providers/VidZee.js +++ b/providers/VidZee.js @@ -1,44 +1,100 @@ +const crypto = require('crypto'); const axios = require('axios'); -// Confirmed AES-CBC decoder for VidZee encrypted link format. -// Format (after initial atob in site script): ivBase64:cipherBase64 -// ivBase64 -> CryptoJS Base64 parsed to WordArray (IV) -// Key: UTF-8 bytes of "qrincywincyspider" padded with nulls to 32 bytes (AES-256-CBC) -// Cipher: base64 ciphertext, PKCS7 padding, mode CBC. -let cryptoJs; // lazy load to avoid cost if not needed -function decodeVidZeeToken(token, debug) { +const BASE_URL = 'https://core.vidzee.wtf'; +const PLAYER_URL = 'https://player.vidzee.wtf'; +const VIDZEE_HEADERS = { + 'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.7051.98 Safari/537.36', + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Accept-Language': 'en-US,en;q=0.9', + 'Referer': PLAYER_URL, + 'Origin': PLAYER_URL +}; + +// Pad/truncate key string to 32 bytes (UTF-8, mirrors CryptoJS behaviour) +function getKeyBytes(key) { + const encoded = Buffer.from(key, 'utf8'); + const result = Buffer.alloc(32); + encoded.copy(result, 0, 0, Math.min(encoded.length, 32)); + return result; +} + +// AES-256-CBC decrypt a VidZee stream link. +// encryptedData is outer-base64 of "ivBase64:cipherBase64" +async function decryptLink(encryptedData, decryptionKey) { try { - if (typeof token !== 'string') return null; - if (/^https?:\/\//i.test(token)) return null; // already a URL - // Expect pattern base64:base64 (iv:cipher) - const raw = Buffer.from(token, 'base64').toString('utf8'); - if (!raw.includes(':')) return null; - const [ivB64, cipherB64] = raw.split(':'); - if (!ivB64 || !cipherB64) return null; - if (!cryptoJs) cryptoJs = require('crypto-js'); - const iv = cryptoJs.enc.Base64.parse(ivB64.trim()); - const keyStr = 'qrincywincyspider'; - // Pad key with nulls to 32 bytes - const keyUtf8 = cryptoJs.enc.Utf8.parse(keyStr.padEnd(32, '\0')); - const decrypted = cryptoJs.AES.decrypt(cipherB64.trim(), keyUtf8, { - iv, - mode: cryptoJs.mode.CBC, - padding: cryptoJs.pad.Pkcs7 - }).toString(cryptoJs.enc.Utf8); - if (!decrypted) return null; - if (!/^https?:\/\//i.test(decrypted)) { - if (debug) console.log('[VidZee] decrypted but not a URL', decrypted.slice(0,80)); - return null; - } - if (debug) console.log('[VidZee] AES decoded token', { tokenSnippet: token.slice(0, 24)+'...', url: decrypted.slice(0,120) }); - return decrypted.trim(); - } catch (e) { - if (debug) console.log('[VidZee] AES decode error', e.message); - return null; + if (!encryptedData || !decryptionKey) return ''; + + const decoded = Buffer.from(encryptedData, 'base64').toString('utf8'); + const colonIdx = decoded.indexOf(':'); + if (colonIdx === -1) return ''; + + const ivBase64 = decoded.slice(0, colonIdx); + const cipherBase64 = decoded.slice(colonIdx + 1); + if (!ivBase64 || !cipherBase64) return ''; + + const iv = Buffer.from(ivBase64, 'base64'); + const cipherBytes = Buffer.from(cipherBase64, 'base64'); + const keyBytes = getKeyBytes(decryptionKey); + + const decipher = crypto.createDecipheriv('aes-256-cbc', keyBytes, iv); + const decrypted = Buffer.concat([decipher.update(cipherBytes), decipher.final()]); + return decrypted.toString('utf8'); + } catch { + return ''; } } -// Removed unused parseArgs helper +// Derive the real AES-CBC decryption key from VidZee's API-key blob via AES-GCM. +// Mirrors the TypeScript deriveKey() in the reference implementation. +async function deriveKey(e) { + try { + if (!e) return ''; + + const t = Buffer.from(e.replace(/\s+/g, ''), 'base64'); + if (t.length <= 28) return ''; + + const n = t.slice(0, 12); // AES-GCM IV + const r = t.slice(12, 28); + const a = t.slice(28); + + // i = a || r (reference: i.set(a, 0); i.set(r, a.length)) + const i = Buffer.concat([a, r]); + + // AES-256-GCM key = SHA-256 of fixed salt string + const l = crypto.createHash('sha256') + .update('4f2a9c7d1e8b3a6f0d5c2e9a7b1f4d8c', 'utf8') + .digest(); + + // WebCrypto passes ciphertext+tag as one buffer; tag is last 16 bytes + const ciphertext = i.slice(0, i.length - 16); + const authTag = i.slice(i.length - 16); + + const decipher = crypto.createDecipheriv('aes-256-gcm', l, n); + decipher.setAuthTag(authTag); + const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]); + return decrypted.toString('utf8'); + } catch { + return ''; + } +} + +// Fetch and derive the current decryption key from VidZee's API +async function fetchDecryptionKey() { + try { + const response = await axios.get(`${BASE_URL}/api-key`, { + headers: VIDZEE_HEADERS, + responseType: 'text', + timeout: 10000 + }); + if (response.status === 200 && response.data) { + return await deriveKey(response.data); + } + return null; + } catch { + return null; + } +} const getVidZeeStreams = async (tmdbId, mediaType, seasonNum, episodeNum) => { if (!tmdbId) { @@ -62,41 +118,31 @@ const getVidZeeStreams = async (tmdbId, mediaType, seasonNum, episodeNum) => { } } - const servers = [1,2,3,4,5,6,7,8,9,10]; // VidZee server identifiers + // Fetch and derive the decryption key before hitting servers + const decKey = await fetchDecryptionKey(); + if (!decKey) { + console.error('[VidZee] Failed to fetch/derive decryption key.'); + return []; + } + console.log('[VidZee] Decryption key derived successfully.'); + + // Servers 0–13 (matches reference implementation) + const servers = Array.from({ length: 14 }, (_, i) => i); const streamPromises = servers.map(async (sr) => { - let targetApiUrl = `https://player.vidzee.wtf/api/server?id=${tmdbId}&sr=${sr}`; - + let apiUrl = `${PLAYER_URL}/api/server?id=${tmdbId}&sr=${sr}`; if (mediaType === 'tv') { - targetApiUrl += `&ss=${seasonNum}&ep=${episodeNum}`; + apiUrl += `&ss=${seasonNum}&ep=${episodeNum}`; } - let finalApiUrl; - const headers = { - 'Referer': `https://player.vidzee.wtf/embed/movie/${tmdbId}` - }; - const timeout = 7000; // Reduced timeout - - finalApiUrl = targetApiUrl; - - console.log(`[VidZee] Fetching from server ${sr}: ${targetApiUrl}`); - try { - const response = await axios.get(finalApiUrl, { - headers: headers, - timeout: timeout + const response = await axios.get(apiUrl, { + headers: VIDZEE_HEADERS, + timeout: 7000 }); const responseData = response.data; - - if (!responseData || typeof responseData !== 'object') { - console.error(`[VidZee S${sr}] Error: Invalid response data from API.`); - return []; - } - - if (responseData.tracks) { - delete responseData.tracks; - } + if (!responseData || typeof responseData !== 'object') return []; let apiSources = []; if (responseData.url && Array.isArray(responseData.url)) { @@ -105,57 +151,51 @@ const getVidZeeStreams = async (tmdbId, mediaType, seasonNum, episodeNum) => { apiSources = [responseData]; } - if (!apiSources || apiSources.length === 0) { - console.log(`[VidZee S${sr}] No stream sources found in API response.`); - return []; - } + if (apiSources.length === 0) return []; - const streams = apiSources.map(sourceItem => { + const streams = await Promise.all(apiSources.map(async (sourceItem) => { const label = sourceItem.name || sourceItem.type || 'VidZee'; let quality = String(label).match(/^\d+$/) ? `${label}p` : label; - if (!/(\d{3,4})p/.test(quality.toLowerCase())) { - quality = '720p'; - } - const language = sourceItem.language || sourceItem.lang; + if (!/\d{3,4}p/.test(quality)) quality = '720p'; + const language = sourceItem.language || sourceItem.lang || 'Unknown'; + let rawLink = sourceItem.link; - const debug = process.env.VIDZEE_DEBUG === '1'; - const decoded = decodeVidZeeToken(rawLink, debug); - if (decoded && /^https?:\/\//i.test(decoded)) { - if (debug) console.log('[VidZee] decoded link', { beforeSample: rawLink.slice(0,40)+'...', after: decoded.slice(0,80) }); - rawLink = decoded; + if (rawLink && !/^https?:\/\//i.test(rawLink)) { + const decoded = await decryptLink(rawLink, decKey); + if (decoded && /^https?:\/\//i.test(decoded)) { + rawLink = decoded; + } else { + console.log(`[VidZee S${sr}] Decryption yielded non-URL, skipping.`); + return null; + } } + + if (!rawLink || !/^https?:\/\//i.test(rawLink)) return null; + return { name: `VidZee Server${sr} - ${quality} - ${language}`, title: `VidZee Server${sr} - ${quality} - ${language}`, url: rawLink, - quality: quality, - provider: "VidZee", - headers: { - 'Referer': 'https://core.vidzee.wtf/' - } + quality, + provider: 'VidZee', + headers: { 'Referer': `${BASE_URL}/` } }; - }).filter(stream => stream.url); + })); - console.log(`[VidZee S${sr}] Successfully extracted ${streams.length} streams. Qualities: ${streams.map(s=>s.quality).join(', ')}`); - return streams; - - } catch (error) { - if (error.response) { - console.error(`[VidZee S${sr}] Error fetching: ${error.response.status} ${error.response.statusText}`); - } else if (error.request) { - console.error(`[VidZee S${sr}] Error fetching: No response received.`); - } else { - console.error(`[VidZee S${sr}] Error fetching:`, error.message); + const valid = streams.filter(Boolean); + if (valid.length > 0) { + console.log(`[VidZee S${sr}] ${valid.length} stream(s) extracted.`); } + return valid; + } catch (error) { + console.error(`[VidZee S${sr}] Error: ${error.message}`); return []; } }); - const allStreamsNested = await Promise.all(streamPromises); - const allStreams = allStreamsNested.flat(); - - console.log(`[VidZee] Found a total of ${allStreams.length} streams from servers ${servers.join(', ')}.`); + const allStreams = (await Promise.all(streamPromises)).flat(); + console.log(`[VidZee] Total streams: ${allStreams.length}`); return allStreams; }; -module.exports = { getVidZeeStreams }; \ No newline at end of file +module.exports = { getVidZeeStreams }; diff --git a/providers/dahmermovies.js b/providers/dahmermovies.js new file mode 100644 index 0000000..85a66ea --- /dev/null +++ b/providers/dahmermovies.js @@ -0,0 +1,152 @@ +const axios = require('axios'); +const { getTmdbApiKey } = require('../utils/tmdbKey'); + +const DAHMER_MOVIES_API = 'https://a.111477.xyz'; +const DAHMER_WORKER_API = 'https://p.111477.xyz/bulk?u='; + +const REQUEST_HEADERS = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', + 'Referer': `${DAHMER_MOVIES_API}/` +}; + +function parseLinks(html) { + const links = []; + const rowRegex = /]*>([\s\S]*?)<\/tr>/gi; + let match; + while ((match = rowRegex.exec(html)) !== null) { + const rowContent = match[1]; + const linkMatch = rowContent.match(/]*href=["']([^"']*)["'][^>]*>([^<]*)<\/a>/i); + const sizeMatch = rowContent.match(/]*>(\d+(?:\.\d+)?\s?[KMGT]B)<\/td>/i); + if (linkMatch) { + const href = linkMatch[1]; + const text = linkMatch[2].trim(); + const size = sizeMatch ? sizeMatch[1].trim() : 'N/A'; + if (text && href !== '../' && /\.(mkv|mp4|avi|webm|m3u8)$/i.test(text)) { + links.push({ text, href, size }); + } + } + } + return links; +} + +async function fetchDirectory(title, year, season) { + const cleanTitle = title.replace(/:/g, ''); + const variants = season !== null ? [ + `/tvs/${encodeURIComponent(cleanTitle)}/Season%20${season < 10 ? '0' + season : season}/`, + `/tvs/${encodeURIComponent(cleanTitle)}/Season%20${season}/` + ] : [ + `/movies/${encodeURIComponent(`${cleanTitle} (${year})`)}/` + ]; + + for (const variant of variants) { + try { + const res = await axios.get(DAHMER_MOVIES_API + variant, { + headers: REQUEST_HEADERS, + timeout: 10000, + responseType: 'text' + }); + if (res.data) return { html: res.data, dirUrl: DAHMER_MOVIES_API + variant }; + } catch { + // try next variant + } + } + return null; +} + +async function getDahmermoviesStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) { + console.log(`[DahmerMovies] Searching for ${mediaType} ${tmdbId}`); + + const tmdbKey = getTmdbApiKey(); + if (!tmdbKey) { + console.error('[DahmerMovies] No TMDB API key configured.'); + return []; + } + + let title, year; + try { + const type = mediaType === 'tv' ? 'tv' : 'movie'; + const { data } = await axios.get( + `https://api.themoviedb.org/3/${type}/${tmdbId}?api_key=${tmdbKey}`, + { timeout: 8000 } + ); + title = data.title || data.name || ''; + year = (data.release_date || data.first_air_date || '').substring(0, 4); + } catch (err) { + console.error(`[DahmerMovies] TMDB lookup failed: ${err.message}`); + return []; + } + + if (!title) return []; + + const dir = await fetchDirectory(title, year, seasonNum); + if (!dir) { + console.log(`[DahmerMovies] Directory not found for "${title}"`); + return []; + } + + let paths = parseLinks(dir.html); + + // For TV, filter to the specific episode when possible + if (seasonNum !== null && episodeNum !== null) { + const epStr = String(episodeNum).padStart(2, '0'); + const seStr = String(seasonNum).padStart(2, '0'); + const epFiltered = paths.filter(p => { + const name = p.text.toLowerCase(); + return name.includes(`s${seStr}e${epStr}`) || name.includes(`e${epStr}`); + }); + if (epFiltered.length > 0) paths = epFiltered; + } + + // Sort 4K first + paths.sort((a, b) => (/2160p|4k/i.test(b.text) ? 1 : 0) - (/2160p|4k/i.test(a.text) ? 1 : 0)); + + const streams = []; + for (const path of paths.slice(0, 5)) { + let directUrl; + if (path.href.startsWith('http')) { + directUrl = path.href; + } else if (path.href.includes('/movies/') || path.href.includes('/tvs/')) { + directUrl = DAHMER_MOVIES_API + (path.href.startsWith('/') ? '' : '/') + path.href; + } else { + directUrl = dir.dirUrl + path.href; + } + directUrl = decodeURI(directUrl.replace(/([^:]\/)\/+/g, '$1')); + + const fileName = path.text; + const isMulti = /\b(HIN|TAM|TEL|Multi|Dual|DUB|Multi-Audio|MULTI)\b/i.test(fileName); + const hasEngTag = /\b(Eng|English)\b/i.test(fileName); + const isEnglishTitle = /^[a-zA-Z0-9\s?!\-:]+$/.test(title); + let language = 'Original'; + if (isMulti) language = 'Multi Audio'; + else if (isEnglishTitle && hasEngTag) language = 'English'; + + const formatMatch = fileName.match(/\.(mkv|mp4|m3u8|avi|webm)$/i); + const fileFormat = formatMatch ? formatMatch[1].toUpperCase() : 'LINK'; + const resolution = fileName.match(/\b(2160p|1080p|720p|4[Kk])\b/)?.[0] || '1080p'; + + const info = fileName + .replace(/\.(mkv|mp4|avi|webm|m3u8)$/i, '') + .replace(/[\[\]()._-]/g, ' ') + .replace(/\s+/g, ' ') + .trim(); + + streams.push({ + name: 'DahmerMovies', + title: `${resolution} | ${language} | ${path.size} | ${fileFormat} | ${info}`, + url: DAHMER_WORKER_API + encodeURI(directUrl), + quality: resolution, + provider: 'DahmerMovies', + headers: { + 'User-Agent': REQUEST_HEADERS['User-Agent'], + 'Referer': `${DAHMER_MOVIES_API}/`, + 'Accept': '*/*', + 'Range': 'bytes=0-' + } + }); + } + + console.log(`[DahmerMovies] Total results found: ${streams.length}`); + return streams; +} + +module.exports = { getDahmermoviesStreams }; diff --git a/providers/lordflix.js b/providers/lordflix.js new file mode 100644 index 0000000..6b92f27 --- /dev/null +++ b/providers/lordflix.js @@ -0,0 +1,118 @@ +const axios = require('axios'); +const { getTmdbApiKey } = require('../utils/tmdbKey'); + +const LORDFLIX_HEADERS = { + 'Accept': '*/*', + 'Origin': 'https://lordflix.org', + 'Referer': 'https://lordflix.org/', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36' +}; + +const LORDFLIX_API = 'https://network.hasta-la-vista.site'; +const MULTI_DECRYPT_API = 'https://enc-dec.app/api'; +const SERVERS = ['Berlin', 'Tokyo', 'Bogota', 'Oslo', 'Luna', 'LordFlix', 'Sakura', 'Rio', 'Ativa']; + +// Matches the reference project's encodeQuote: encodeURIComponent with + encoded as %20 +function encodeQuote(str) { + return encodeURIComponent(str).replace(/%20/g, '+').replace(/\+/g, '%20'); +} + +async function getLordflixStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) { + console.log(`[Lordflix] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`); + + const tmdbKey = getTmdbApiKey(); + if (!tmdbKey) { + console.error('[Lordflix] No TMDB API key configured.'); + return []; + } + + // Step 1: TMDB lookup for title, year, imdbId + let info; + try { + const type = mediaType === 'tv' ? 'tv' : 'movie'; + const { data } = await axios.get( + `https://api.themoviedb.org/3/${type}/${tmdbId}?api_key=${tmdbKey}&append_to_response=external_ids`, + { timeout: 8000 } + ); + info = { + title: data.title || data.name || '', + year: (data.release_date || data.first_air_date || '').split('-')[0], + imdbId: (data.external_ids && data.external_ids.imdb_id) || '' + }; + } catch (err) { + console.error(`[Lordflix] TMDB lookup failed: ${err.message}`); + return []; + } + + if (!info.title || !info.imdbId) { + console.error('[Lordflix] Missing title or IMDb ID from TMDB.'); + return []; + } + + const typeParam = mediaType === 'tv' ? 'series' : 'movie'; + const titleEnc = encodeQuote(info.title); + const streams = []; + + // Step 2: Query each server in parallel + await Promise.all(SERVERS.map(async (server) => { + try { + let serverUrl = `${LORDFLIX_API}/?title=${titleEnc}&type=${typeParam}&year=${info.year || ''}` + + `&imdb=${info.imdbId}&tmdb=${tmdbId}&server=${server}`; + if (mediaType === 'tv') { + serverUrl += `&season=${seasonNum}&episode=${episodeNum}`; + } + + // Step 3: Get signed proxy URL from enc-dec.app + const encBridgeRes = await axios.get( + `${MULTI_DECRYPT_API}/enc-lordflix?url=${encodeQuote(serverUrl)}`, + { timeout: 8000 } + ); + const encBridgeJson = encBridgeRes.data; + if (!encBridgeJson || encBridgeJson.status !== 200 || !encBridgeJson.result) return; + + const { url: proxyEncUrl, sign: signature } = encBridgeJson.result; + if (!proxyEncUrl || !signature) return; + + // Step 4: Fetch encrypted stream data from proxy URL + const remoteEncRes = await axios.get(proxyEncUrl, { + headers: LORDFLIX_HEADERS, + timeout: 8000, + responseType: 'text' + }); + const remoteEncData = typeof remoteEncRes.data === 'string' + ? remoteEncRes.data + : JSON.stringify(remoteEncRes.data); + + // Step 5: Decrypt via enc-dec.app + const decRes = await axios.post(`${MULTI_DECRYPT_API}/dec-lordflix`, + { text: remoteEncData, sign: signature }, + { headers: { 'Content-Type': 'application/json' }, timeout: 8000 } + ); + const finalJson = decRes.data; + if (!finalJson || finalJson.status !== 200 || !finalJson.result || finalJson.result.error) return; + + const streamList = finalJson.result.stream; + if (!Array.isArray(streamList) || streamList.length === 0) return; + + const topStream = streamList[0]; + if (topStream.type === 'hls' && topStream.playlist) { + streams.push({ + name: `Lordflix[${server}]`, + title: `Lordflix[${server}]`, + url: topStream.playlist, + quality: 'Auto', + provider: 'Lordflix', + headers: LORDFLIX_HEADERS + }); + console.log(`[Lordflix] Server ${server}: got stream.`); + } + } catch (err) { + console.error(`[Lordflix] Server ${server} error: ${err.message}`); + } + })); + + console.log(`[Lordflix] Total streams: ${streams.length}`); + return streams; +} + +module.exports = { getLordflixStreams }; diff --git a/providers/moviesmod.js b/providers/moviesmod.js index a0cb631..d3d054e 100644 --- a/providers/moviesmod.js +++ b/providers/moviesmod.js @@ -202,7 +202,7 @@ async function extractDownloadLinks(moviePageUrl) { if (header.is('h3') && headerText.toLowerCase().includes('season')) { // TV Show Logic - const linkElements = blockContent.find('a.maxbutton-episode-links, a.maxbutton-batch-zip'); + const linkElements = blockContent.find('a.maxbutton-episode-links, a.maxbutton-batch-zip, a.maxbutton-download-links, a[href*="links.modpro.blog"], a[href*="posts.modpro.blog"]'); linkElements.each((j, linkEl) => { const buttonText = $(linkEl).text().trim(); const linkUrl = $(linkEl).attr('href'); @@ -214,15 +214,17 @@ async function extractDownloadLinks(moviePageUrl) { } }); } else if (header.is('h4')) { - // Movie Logic - const linkElement = blockContent.find('a[href*="modrefer.in"]').first(); + // Movie Logic - site changed from modrefer.in to links.modpro.blog + const linkElement = blockContent.find('a.maxbutton-download-links, a[href*="links.modpro.blog"], a[href*="posts.modpro.blog"], a[href*="modrefer.in"]').first(); if (linkElement.length > 0) { const link = linkElement.attr('href'); const cleanQuality = extractQuality(headerText); - links.push({ - quality: cleanQuality, - url: link - }); + if (link && cleanQuality && cleanQuality !== 'Unknown') { + links.push({ + quality: cleanQuality, + url: link + }); + } } } }); @@ -239,7 +241,35 @@ async function resolveIntermediateLink(initialUrl, refererUrl, quality) { try { const urlObject = new URL(initialUrl); - if (urlObject.hostname.includes('dramadrip.com')) { + if (urlObject.hostname.includes('links.modpro.blog') || urlObject.hostname.includes('posts.modpro.blog')) { + const { data } = await makeRequest(initialUrl, { headers: { 'Referer': refererUrl } }); + const $ = cheerio.load(data); + const finalLinks = []; + + // Primary: scoped to .entry-content + $('.entry-content a[href*="driveseed.org"], .entry-content a[href*="cloud.unblockedgames.world"], .entry-content a[href*="tech.creativeexpressionsblog.com"], .entry-content a[href*="tech.examzculture.in"], .entry-content a[href*="tech.unblockedgames.world"], .entry-content a[href*="tech.examdegree.site"]').each((i, el) => { + const link = $(el).attr('href'); + const text = $(el).text().trim(); + if (link && text && !text.toLowerCase().includes('batch')) { + finalLinks.push({ server: text.replace(/\s+/g, ' '), url: link }); + } + }); + + // Fallback: page-wide search + if (finalLinks.length === 0) { + $('a[href*="driveseed.org"], a[href*="cloud.unblockedgames.world"], a[href*="tech.creativeexpressionsblog.com"], a[href*="tech.examzculture.in"], a[href*="tech.unblockedgames.world"], a[href*="tech.examdegree.site"]').each((i, el) => { + const link = $(el).attr('href'); + const text = $(el).text().trim(); + if (link && text && !text.toLowerCase().includes('batch')) { + finalLinks.push({ server: text.replace(/\s+/g, ' ') || 'Download Link', url: link }); + } + }); + } + + console.log(`[MoviesMod] Found ${finalLinks.length} links from ${urlObject.hostname}`); + return finalLinks; + + } else if (urlObject.hostname.includes('dramadrip.com')) { const { data: dramaData } = await makeRequest(initialUrl, { headers: { 'Referer': refererUrl } }); const $$ = cheerio.load(dramaData); @@ -779,18 +809,38 @@ async function getMoviesModStreams(tmdbId, mediaType, seasonNum = null, episodeN } const { default: fetch } = await import('node-fetch'); - const tmdbUrl = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY}&language=en-US`; + const tmdbUrl = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY}&language=en-US&append_to_response=external_ids`; const tmdbDetails = await (await fetch(tmdbUrl)).json(); const title = mediaType === 'tv' ? tmdbDetails.name : tmdbDetails.title; const year = mediaType === 'tv' ? tmdbDetails.first_air_date?.substring(0, 4) : tmdbDetails.release_date?.substring(0, 4); + const imdbId = tmdbDetails.external_ids?.imdb_id || null; if (!title) throw new Error('Could not get title from TMDB'); - console.log(`[MoviesMod] Found metadata: ${title} (${year})`); - const searchResults = await searchMoviesMod(title); + console.log(`[MoviesMod] Found metadata: ${title} (${year})${imdbId ? ` [IMDB: ${imdbId}]` : ''}`); + + // Try IMDb ID search first — more reliable than title matching + let searchResults = []; + if (imdbId) { + const imdbQuery = (mediaType === 'tv' && seasonNum) ? `${imdbId} Season ${seasonNum}` : imdbId; + console.log(`[MoviesMod] Trying IMDB ID search: ${imdbQuery}`); + searchResults = await searchMoviesMod(imdbQuery); + if (searchResults.length > 0) { + console.log(`[MoviesMod] Found ${searchResults.length} results via IMDB ID`); + } + } + + if (searchResults.length === 0) { + console.log(`[MoviesMod] Falling back to title search: ${title}`); + searchResults = await searchMoviesMod(title); + if (searchResults.length === 0 && mediaType === 'tv' && seasonNum) { + searchResults = await searchMoviesMod(`${title} Season ${seasonNum}`); + } + } + if (searchResults.length === 0) throw new Error(`No search results found for "${title}"`); - // --- NEW: Use string similarity to find the best match --- + // --- Use string similarity to find the best match --- const titles = searchResults.map(r => r.title); const bestMatch = findBestMatch(title, titles); @@ -853,21 +903,27 @@ async function getMoviesModStreams(tmdbId, mediaType, seasonNum = null, episodeN const finalLinks = await resolveIntermediateLink(link.url, selectedResult.url, link.quality); if (!finalLinks || finalLinks.length === 0) return null; - // Resolve to driveseed redirect URLs (intermediate step) for caching + // Resolve to host redirect URLs (intermediate step) for caching const driveseedPromises = finalLinks.map(async (targetLink) => { try { let currentUrl = targetLink.url; - // Handle SID links if they appear - if (currentUrl.includes('tech.unblockedgames.world') || currentUrl.includes('tech.creativeexpressionsblog.com') || currentUrl.includes('tech.examzculture.in')) { + // Handle SID links (all known tech-unblock domains) + if ( + currentUrl.includes('tech.unblockedgames.world') || + currentUrl.includes('cloud.unblockedgames.world') || + currentUrl.includes('tech.creativeexpressionsblog.com') || + currentUrl.includes('tech.examzculture.in') || + currentUrl.includes('tech.examdegree.site') + ) { const resolvedUrl = await resolveTechUnblockedLink(currentUrl); if (!resolvedUrl) return null; currentUrl = resolvedUrl; } - // Only process if it's a driveseed URL - if (currentUrl && currentUrl.includes('driveseed.org')) { - console.log(`[MoviesMod] Caching driveseed redirect URL for ${targetLink.server}: ${currentUrl}`); + // Accept driveseed.org or driveleech.net (same operator, same page structure) + if (currentUrl && (currentUrl.includes('driveseed.org') || currentUrl.includes('driveleech.net'))) { + console.log(`[MoviesMod] Resolved host URL for ${targetLink.server}: ${currentUrl}`); return { ...targetLink, driveseedRedirectUrl: currentUrl }; } return null; @@ -932,9 +988,11 @@ async function getMoviesModStreams(tmdbId, mediaType, seasonNum = null, episodeN const { driveseedRedirectUrl } = targetLink; if (!driveseedRedirectUrl) return null; - // Process the cached driveseed redirect URL - if (driveseedRedirectUrl.includes('driveseed.org')) { - // First, resolve the driveseed redirect URL to get the final file page URL + // Process the cached host redirect URL (driveseed.org or driveleech.net) + if (driveseedRedirectUrl.includes('driveseed.org') || driveseedRedirectUrl.includes('driveleech.net')) { + const hostOrigin = new URL(driveseedRedirectUrl).origin; + + // Resolve the redirect URL to get the final file page URL const response = await makeRequest(driveseedRedirectUrl, { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', @@ -949,7 +1007,8 @@ async function getMoviesModStreams(tmdbId, mediaType, seasonNum = null, episodeN let $; // single declaration for cheerio instance if (redirectMatch && redirectMatch[1]) { const finalPath = redirectMatch[1]; - finalFilePageUrl = `https://driveseed.org${finalPath}`; + // Use the dynamic host origin — works for both driveseed.org and driveleech.net + finalFilePageUrl = finalPath.startsWith('http') ? finalPath : `${hostOrigin}${finalPath}`; console.log(`[MoviesMod] Resolved redirect to final file page: ${finalFilePageUrl}`); // Load the final file page @@ -996,7 +1055,7 @@ async function getMoviesModStreams(tmdbId, mediaType, seasonNum = null, episodeN downloadOptions.push({ title: 'Resume Cloud', type: 'resume', - url: `https://driveseed.org${resumeCloudLink}`, + url: resumeCloudLink.startsWith('http') ? resumeCloudLink : `${hostOrigin}${resumeCloudLink}`, priority: 1 }); } diff --git a/providers/notorrent.js b/providers/notorrent.js new file mode 100644 index 0000000..a6356ca --- /dev/null +++ b/providers/notorrent.js @@ -0,0 +1,92 @@ +const axios = require('axios'); +const { getTmdbApiKey } = require('../utils/tmdbKey'); + +const NOTORRENT_API = 'https://addon-osvh.onrender.com'; + +function cleanText(str) { + if (!str) return ''; + return str.replace(/[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]/gu, '').trim(); +} + +function extractQuality(titleText) { + const raw = titleText || ''; + const match = raw.match(/(\d{3,4}p)/); + if (match) return match[0]; + if (raw.toUpperCase().includes('FREE')) return 'Auto'; + return 'Unknown'; +} + +async function getNotorrentStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) { + console.log(`[NoTorrent] Searching for ${mediaType} ${tmdbId}`); + + const tmdbKey = getTmdbApiKey(); + if (!tmdbKey) { + console.error('[NoTorrent] No TMDB API key configured.'); + return []; + } + + // Step 1: TMDB → IMDB ID + let imdbId; + try { + const type = mediaType === 'tv' ? 'tv' : 'movie'; + const { data } = await axios.get( + `https://api.themoviedb.org/3/${type}/${tmdbId}?api_key=${tmdbKey}&append_to_response=external_ids`, + { timeout: 8000 } + ); + imdbId = (data.external_ids && data.external_ids.imdb_id) || null; + } catch (err) { + console.error(`[NoTorrent] TMDB lookup failed: ${err.message}`); + return []; + } + + if (!imdbId) { + console.warn('[NoTorrent] Failed to map IMDB ID from TMDB.'); + return []; + } + + // Step 2: Fetch streams from NoTorrent Stremio addon API + const apiUrl = (mediaType === 'tv' && seasonNum != null) + ? `${NOTORRENT_API}/stream/series/${imdbId}:${seasonNum}:${episodeNum}.json` + : `${NOTORRENT_API}/stream/movie/${imdbId}.json`; + + try { + const { data } = await axios.get(apiUrl, { timeout: 20000 }); + const rawList = data.streams || []; + const streams = []; + + for (const item of rawList) { + if (item.externalUrl || !item.url) continue; + if (item.url.includes('github.com') || item.url.includes('googleusercontent')) continue; + + const cleanTitleStr = cleanText(item.title || ''); + const quality = extractQuality(cleanTitleStr); + + let language = 'Default'; + const langMatch = cleanTitleStr.match(/\(([^)]+)\)/); + if (langMatch) { + language = langMatch[1].charAt(0).toUpperCase() + langMatch[1].slice(1).toLowerCase(); + } + + const proxyHeaders = (item.behaviorHints?.proxyHeaders?.request) || {}; + const headers = { ...(item.behaviorHints?.headers || {}), ...proxyHeaders }; + + const nameParts = ['NoTorrent', language !== 'Default' ? language : ''].filter(p => p.trim() !== ''); + streams.push({ + name: nameParts.join(' \u2022 '), + title: quality, + url: item.url, + quality, + provider: 'NoTorrent', + headers: Object.keys(headers).length > 0 ? headers : undefined + }); + } + + console.log(`[NoTorrent] Total results found: ${streams.length}`); + return streams; + } catch (err) { + console.error(`[NoTorrent] Fetch failed: ${err.message}`); + return []; + } +} + +module.exports = { getNotorrentStreams }; diff --git a/providers/registry.js b/providers/registry.js index f6e0804..c976763 100644 --- a/providers/registry.js +++ b/providers/registry.js @@ -10,10 +10,13 @@ const providerFunctionMap = { 'Showbox.js': 'getStreamsFromTmdbId', '4khdhub.js': 'get4KHDHubStreams', 'moviesmod.js': 'getMoviesModStreams', - 'MP4Hydra.js': 'getMP4HydraStreams', 'VidZee.js': 'getVidZeeStreams', 'vixsrc.js': 'getVixsrcStreams', - 'uhdmovies.js': 'getUHDMoviesStreams', + 'videasy.js': 'getVideasyStreams', + 'vidlink.js': 'getVidlinkStreams', + 'lordflix.js': 'getLordflixStreams', + 'notorrent.js': 'getNotorrentStreams', + 'dahmermovies.js': 'getDahmermoviesStreams', }; // Stats for debug endpoint @@ -27,7 +30,7 @@ async function getEffectiveCookies() { function getProviderFiles() { const providersDir = path.join(__dirname); return fs.readdirSync(providersDir) - .filter(file => file.endsWith('.js') && file !== 'registry.js' && file !== 'vidsrcextractor.js') + .filter(file => file.endsWith('.js') && file !== 'registry.js') .map(file => ({ name: path.parse(file).name.toLowerCase(), file: file, @@ -110,28 +113,29 @@ function createFetchFunction(providerInfo) { }; } -// Initialize providers +// Initialize providers - always load all; enabled state is checked dynamically from live config const providerFiles = getProviderFiles(); const providers = []; for (const providerInfo of providerFiles) { - const enableFlag = `enable${providerInfo.name.charAt(0).toUpperCase() + providerInfo.name.slice(1)}Provider`; - const enabled = config[enableFlag] !== false; - - if (enabled) { - providers.push({ - name: providerInfo.name, - enabled: true, - fetch: createFetchFunction(providerInfo) - }); - console.log(`[registry] ${providerInfo.name} provider enabled`); - } else { - console.log(`[registry] ${providerInfo.name} disabled via config`); - } + providers.push({ + name: providerInfo.name, + fetch: createFetchFunction(providerInfo) + }); + console.log(`[registry] ${providerInfo.name} provider loaded`); } -function listProviders() { return providers.map(p => ({ name: p.name, enabled: p.enabled })); } -function getProvider(name) { return providers.find(p => p.name === name.toLowerCase()) || null; } +function isProviderEnabled(name) { + const flag = `enable${name.charAt(0).toUpperCase() + name.slice(1)}Provider`; + return config[flag] !== false; +} + +function listProviders() { return providers.map(p => ({ name: p.name, enabled: isProviderEnabled(p.name) })); } +function getProvider(name) { + const p = providers.find(p => p.name === name.toLowerCase()); + if (!p) return null; + return { ...p, enabled: isProviderEnabled(p.name) }; +} function getCookieStats() { return lastCookieStats; } diff --git a/providers/uhdmovies.js b/providers/uhdmovies.js deleted file mode 100644 index d2c7ff2..0000000 --- a/providers/uhdmovies.js +++ /dev/null @@ -1,1418 +0,0 @@ -const axios = require('axios'); -const cheerio = require('cheerio'); -const { URLSearchParams, URL } = require('url'); -const FormData = require('form-data'); -const { CookieJar } = require('tough-cookie'); -const fs = require('fs').promises; -const path = require('path'); - -// Dynamic import for axios-cookiejar-support -let axiosCookieJarSupport = null; -const getAxiosCookieJarSupport = async () => { - if (!axiosCookieJarSupport) { - axiosCookieJarSupport = await import('axios-cookiejar-support'); - } - return axiosCookieJarSupport; -}; - -// All requests are direct - -// --- Domain Fetching --- -let uhdMoviesDomain = 'https://uhdmovies.email'; // Fallback domain -let domainCacheTimestamp = 0; -const DOMAIN_CACHE_TTL = 4 * 60 * 60 * 1000; // 4 hours - -async function getUHDMoviesDomain() { - const now = Date.now(); - if (now - domainCacheTimestamp < DOMAIN_CACHE_TTL) { - return uhdMoviesDomain; - } - - try { - console.log('[UHDMovies] Fetching latest domain...'); - const response = await makeRequest('https://raw.githubusercontent.com/phisher98/TVVVV/refs/heads/main/domains.json', { timeout: 10000 }); - if (response.data && response.data.UHDMovies) { - uhdMoviesDomain = response.data.UHDMovies; - domainCacheTimestamp = now; - console.log(`[UHDMovies] Updated domain to: ${uhdMoviesDomain}`); - } else { - console.warn('[UHDMovies] Domain JSON fetched, but "UHDMovies" key was not found. Using fallback.'); - } - } catch (error) { - console.error(`[UHDMovies] Failed to fetch latest domain, using fallback. Error: ${error.message}`); - } - return uhdMoviesDomain; -} - -// Constants -const { getTmdbApiKey } = require('../utils/tmdbKey'); -function getUhdMoviesKey() { - const k = getTmdbApiKey(); - if (!k) console.warn('[UHDMovies] TMDB API key missing; certain metadata features skipped.'); - return k; -} - -// --- Caching Configuration --- -const CACHE_ENABLED = process.env.DISABLE_CACHE !== 'true'; // Set to true to disable caching for this provider -console.log(`[UHDMovies] Internal cache is ${CACHE_ENABLED ? 'enabled' : 'disabled'}.`); -const CACHE_DIR = process.env.VERCEL ? path.join('/tmp', '.uhd_cache') : path.join(__dirname, '.cache', 'uhdmovies'); // Cache directory inside providers/uhdmovies - -// Caching layer simplified: placeholders only (extend for FS persistence if needed) - -// --- Caching Helper Functions --- -const ensureCacheDir = async () => { - if (!CACHE_ENABLED) return; - try { - await fs.mkdir(CACHE_DIR, { recursive: true }); - } catch (error) { - if (error.code !== 'EEXIST') { - console.error(`[UHDMovies Cache] Error creating cache directory: ${error.message}`); - } - } -}; - -const getFromCache = async (key) => { - if (!CACHE_ENABLED) return null; - return null; // no-op -}; - -const saveToCache = async (key, data) => { - if (!CACHE_ENABLED) return; - // no-op (placeholder for future FS caching) -}; - -// Initialize cache directory on startup -ensureCacheDir(); - -// Configure axios with headers to mimic a browser -const createAxiosInstance = () => { - const config = { - headers: { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', - 'Accept-Language': 'en-US,en;q=0.5', - 'Connection': 'keep-alive', - 'Upgrade-Insecure-Requests': '1', - 'Cache-Control': 'max-age=0' - }, - timeout: 30000 - }; - - return axios.create(config); -}; - -const axiosInstance = createAxiosInstance(); - -// Request helper -const makeRequest = async (url, options = {}) => { - console.log(`[UHDMovies] Making direct request to: ${url}`); - return axiosInstance.get(url, options); -}; - -// Removed unused in-memory cache structure (uhdMoviesCache) - -// Function to search for movies -async function searchMovies(query) { - try { - const baseUrl = await getUHDMoviesDomain(); - console.log(`[UHDMovies] Searching for: ${query}`); - const searchUrl = `${baseUrl}/search/${encodeURIComponent(query)}`; - - const response = await makeRequest(searchUrl); - const $ = cheerio.load(response.data); - - const searchResults = []; - - // New logic for grid-based search results - $('article.gridlove-post').each((index, element) => { - const linkElement = $(element).find('a[href*="/download-"]'); - if (linkElement.length > 0) { - const link = linkElement.first().attr('href'); - // Prefer the 'title' attribute, fallback to h1 text - const title = linkElement.first().attr('title') || $(element).find('h1.sanket').text().trim(); - - if (link && title && !searchResults.some(item => item.link === link)) { - searchResults.push({ - title, - link: link.startsWith('http') ? link : `${baseUrl}${link}` - }); - } - } - }); - - // Fallback for original list-based search if new logic fails - if (searchResults.length === 0) { - console.log('[UHDMovies] Grid search logic found no results, trying original list-based logic...'); - $('a[href*="/download-"]').each((index, element) => { - const link = $(element).attr('href'); - // Avoid duplicates by checking if link already exists in results - if (link && !searchResults.some(item => item.link === link)) { - const title = $(element).text().trim(); - if (title) { - searchResults.push({ - title, - link: link.startsWith('http') ? link : `${baseUrl}${link}` - }); - } - } - }); - } - - console.log(`[UHDMovies] Found ${searchResults.length} results`); - return searchResults; - } catch (error) { - console.error(`[UHDMovies] Error searching movies: ${error.message}`); - return []; - } -} - -// Function to extract clean quality information from verbose text -function extractCleanQuality(fullQualityText) { - if (!fullQualityText || fullQualityText === 'Unknown Quality') { - return 'Unknown Quality'; - } - - const cleanedFullQualityText = fullQualityText.replace(/(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])/g, '').trim(); - const text = cleanedFullQualityText.toLowerCase(); - const quality = []; - - // Extract resolution - if (text.includes('2160p') || text.includes('4k')) { - quality.push('4K'); - } else if (text.includes('1080p')) { - quality.push('1080p'); - } else if (text.includes('720p')) { - quality.push('720p'); - } else if (text.includes('480p')) { - quality.push('480p'); - } - - // Extract special features - if (text.includes('hdr')) { - quality.push('HDR'); - } - if (text.includes('dolby vision') || text.includes('dovi') || /\bdv\b/.test(text)) { - quality.push('DV'); - } - if (text.includes('imax')) { - quality.push('IMAX'); - } - if (text.includes('bluray') || text.includes('blu-ray')) { - quality.push('BluRay'); - } - - // If we found any quality indicators, join them - if (quality.length > 0) { - return quality.join(' | '); - } - - // Fallback: try to extract a shorter version of the original text - // Look for patterns like "Movie Name (Year) Resolution ..." - const patterns = [ - /(\d{3,4}p.*?(?:x264|x265|hevc).*?)[[(]/i, - /(\d{3,4}p.*?)[[(]/i, - /((?:720p|1080p|2160p|4k).*?)$/i - ]; - - for (const pattern of patterns) { - const match = cleanedFullQualityText.match(pattern); - if (match && match[1].trim().length < 100) { - return match[1].trim().replace(/x265/ig, 'HEVC'); - } - } - - // Final fallback: truncate if too long - if (cleanedFullQualityText.length > 80) { - return cleanedFullQualityText.substring(0, 77).replace(/x265/ig, 'HEVC') + '...'; - } - - return cleanedFullQualityText.replace(/x265/ig, 'HEVC'); -} - -// Function to extract download links for TV shows from a page -async function extractTvShowDownloadLinks(showPageUrl, season, episode) { - try { - console.log(`[UHDMovies] Extracting TV show links from: ${showPageUrl} for S${season}E${episode}`); - const response = await makeRequest(showPageUrl); - const $ = cheerio.load(response.data); - - const showTitle = $('h1').first().text().trim(); - const downloadLinks = []; - - // --- NEW LOGIC TO SCOPE SEARCH TO THE CORRECT SEASON --- - let inTargetSeason = false; - let qualityText = ''; - - $('.entry-content').find('*').each((index, element) => { - const $el = $(element); - const text = $el.text().trim(); - const seasonMatch = text.match(/^SEASON\s+(\d+)/i); - - // Check if we are entering a new season block - if (seasonMatch) { - const currentSeasonNum = parseInt(seasonMatch[1], 10); - if (currentSeasonNum === season) { - inTargetSeason = true; - console.log(`[UHDMovies] Entering Season ${season} block.`); - } else if (inTargetSeason) { - // We've hit the next season, so we stop. - console.log(`[UHDMovies] Exiting Season ${season} block, now in Season ${currentSeasonNum}.`); - inTargetSeason = false; - return false; // Exit .each() loop - } - } - - if (inTargetSeason) { - // This element is within the correct season's block. - - // Is this a quality header? (e.g., a
 or a 

with ) - // It often contains resolution, release group, etc. - const isQualityHeader = $el.is('pre, p:has(strong), p:has(b), h3, h4'); - if (isQualityHeader) { - const headerText = $el.text().trim(); - // Filter out irrelevant headers. We can be more aggressive here. - if (headerText.length > 5 && !/plot|download|screenshot|trailer|join|powered by|season/i.test(headerText) && !($el.find('a').length > 0)) { - qualityText = headerText; // Store the most recent quality header - } - } - - // Is this a paragraph with episode links? - if ($el.is('p') && $el.find('a[href*="tech.unblockedgames.world"], a[href*="tech.examzculture.in"]').length > 0) { - const linksParagraph = $el; - const episodeRegex = new RegExp(`^Episode\\s+0*${episode}(?!\\d)`, 'i'); - const targetEpisodeLink = linksParagraph.find('a').filter((i, el) => { - return episodeRegex.test($(el).text().trim()); - }).first(); - - if (targetEpisodeLink.length > 0) { - const link = targetEpisodeLink.attr('href'); - if (link && !downloadLinks.some(item => item.link === link)) { - const sizeMatch = qualityText.match(/\[\s*([0-9.,]+\s*[KMGT]B)/i); - const size = sizeMatch ? sizeMatch[1] : 'Unknown'; - - const cleanQuality = extractCleanQuality(qualityText); - const rawQuality = qualityText.replace(/(\r\n|\n|\r)/gm, " ").replace(/\s+/g, ' ').trim(); - - console.log(`[UHDMovies] Found match: Quality='${qualityText}', Link='${link}'`); - downloadLinks.push({ quality: cleanQuality, size: size, link: link, rawQuality: rawQuality }); - } - } - } - - // --- ENHANCED: Check for maxbutton-gdrive-episode structure --- - if ($el.is('p') && $el.find('a.maxbutton-gdrive-episode').length > 0) { - const episodeRegex = new RegExp(`^Episode\\s+0*${episode}(?!\\d)`, 'i'); - const targetEpisodeLink = $el.find('a.maxbutton-gdrive-episode').filter((i, el) => { - const episodeText = $(el).find('.mb-text').text().trim(); - return episodeRegex.test(episodeText); - }).first(); - - if (targetEpisodeLink.length > 0) { - const link = targetEpisodeLink.attr('href'); - if (link && !downloadLinks.some(item => item.link === link)) { - const sizeMatch = qualityText.match(/\[\s*([0-9.,]+\s*[KMGT]B)/i); - const size = sizeMatch ? sizeMatch[1] : 'Unknown'; - - const cleanQuality = extractCleanQuality(qualityText); - const rawQuality = qualityText.replace(/(\r\n|\n|\r)/gm, " ").replace(/\s+/g, ' ').trim(); - - console.log(`[UHDMovies] Found match (maxbutton): Quality='${qualityText}', Link='${link}'`); - downloadLinks.push({ quality: cleanQuality, size: size, link: link, rawQuality: rawQuality }); - } - } - } - } - }); - - if (downloadLinks.length === 0) { - console.log('[UHDMovies] Main extraction logic failed. Checking if requested season exists on page before fallback.'); - - // Check if the requested season exists on the page at all - let seasonExists = false; - const actualSeasonsOnPage = new Set(); // Track what seasons actually have content - - // First pass: Look for actual episode content to see what seasons are available - $('.entry-content').find('a[href*="tech.unblockedgames.world"], a[href*="tech.examzculture.in"], a.maxbutton-gdrive-episode').each((index, element) => { - const $el = $(element); - const linkText = $el.text().trim(); - const episodeText = $el.find('.mb-text').text().trim() || linkText; - - // Look for season indicators in episode links - const seasonMatches = [ - episodeText.match(/S(\d{1,2})/i), // S01, S02, etc. - episodeText.match(/Season\s+(\d+)/i), // Season 1, Season 2, etc. - episodeText.match(/S(\d{1,2})E(\d{1,3})/i) // S01E01 format - ]; - - for (const match of seasonMatches) { - if (match && match[1]) { - const foundSeason = parseInt(match[1], 10); - actualSeasonsOnPage.add(foundSeason); - } - } - }); - - console.log(`[UHDMovies] Actual seasons found on page: ${Array.from(actualSeasonsOnPage).sort((a, b) => a - b).join(', ')}`); - - // Check if requested season is in the actual content - if (actualSeasonsOnPage.has(season)) { - seasonExists = true; - console.log(`[UHDMovies] Season ${season} confirmed to exist in actual episode content`); - } else { - // Fallback: Check page descriptions/titles for season mentions - $('.entry-content').find('*').each((index, element) => { - const $el = $(element); - const text = $el.text().trim(); - // Match various season formats: "SEASON 2", "Season 2", "(Season 1 – 2)", "Season 1-2", etc. - const seasonMatches = [ - text.match(/^SEASON\s+(\d+)/i), - text.match(/\bSeason\s+(\d+)/i), - text.match(/\(Season\s+\d+\s*[–-]\s*(\d+)\)/i), // Matches "(Season 1 – 2)" - text.match(/Season\s+\d+\s*[–-]\s*(\d+)/i), // Matches "Season 1-2" - text.match(/\bS(\d+)/i) // Matches "S2", "S02", etc. - ]; - - for (const match of seasonMatches) { - if (match) { - const currentSeasonNum = parseInt(match[1], 10); - if (currentSeasonNum === season) { - seasonExists = true; - console.log(`[UHDMovies] Season ${season} found in page description: "${text.substring(0, 100)}..."`); - return false; // Exit .each() loop - } - // For range formats like "Season 1 – 2", check if requested season is in range - if (match[0].includes('–') || match[0].includes('-')) { - const rangeMatch = match[0].match(/Season\s+(\d+)\s*[–-]\s*(\d+)/i); - if (rangeMatch) { - const startSeason = parseInt(rangeMatch[1], 10); - const endSeason = parseInt(rangeMatch[2], 10); - if (season >= startSeason && season <= endSeason) { - seasonExists = true; - console.log(`[UHDMovies] Season ${season} found in range ${startSeason}-${endSeason} in page description`); - return false; // Exit .each() loop - } - } - } - } - } - }); - } - - if (!seasonExists) { - console.log(`[UHDMovies] Season ${season} not found on page. Available seasons may not include the requested season.`); - // Don't use fallback if the season doesn't exist to avoid wrong episodes - return { title: showTitle, links: [], seasonNotFound: true }; - } - - console.log(`[UHDMovies] Season ${season} exists on page but episode extraction failed. Trying fallback method with season filtering.`); - - // --- ENHANCED FALLBACK LOGIC FOR NEW HTML STRUCTURE --- - // Try the new maxbutton-gdrive-episode structure first - $('.entry-content').find('a.maxbutton-gdrive-episode').each((i, el) => { - const linkElement = $(el); - const episodeText = linkElement.find('.mb-text').text().trim(); - const episodeRegex = new RegExp(`^Episode\\s+0*${episode}(?!\\d)`, 'i'); - - if (episodeRegex.test(episodeText)) { - const link = linkElement.attr('href'); - if (link && !downloadLinks.some(item => item.link === link)) { - let qualityText = 'Unknown Quality'; - - // Look for quality info in the preceding paragraph or heading - const parentP = linkElement.closest('p, div'); - const prevElement = parentP.prev(); - if (prevElement.length > 0) { - const prevText = prevElement.text().trim(); - if (prevText && prevText.length > 5 && !prevText.toLowerCase().includes('download')) { - qualityText = prevText; - } - } - - // Check if this episode belongs to the correct season - // Enhanced season check - look for various season formats - const seasonCheckRegexes = [ - new RegExp(`\\.S0*${season}[\\.]`, 'i'), // .S01. - new RegExp(`S0*${season}[\\.]`, 'i'), // S01. - new RegExp(`S0*${season}\\b`, 'i'), // S01 (word boundary) - new RegExp(`Season\\s+0*${season}\\b`, 'i'), // Season 1 - new RegExp(`S0*${season}`, 'i') // S01 anywhere - ]; - - const seasonMatch = seasonCheckRegexes.some(regex => regex.test(qualityText)); - if (!seasonMatch) { - console.log(`[UHDMovies] Skipping episode from different season: Quality='${qualityText}'`); - return; // Skip this episode as it's from a different season - } - - const sizeMatch = qualityText.match(/\[([0-9.,]+[KMGT]B[^\]]*)\]/i); - const size = sizeMatch ? sizeMatch[1] : 'Unknown'; - const cleanQuality = extractCleanQuality(qualityText); - const rawQuality = qualityText.replace(/(\r\n|\n|\r)/gm, " ").replace(/\s+/g, ' ').trim(); - - console.log(`[UHDMovies] Found match via enhanced fallback (maxbutton): Quality='${qualityText}', Link='${link}'`); - downloadLinks.push({ quality: cleanQuality, size: size, link: link, rawQuality: rawQuality }); - } - } - }); - - // If still no results, try the original fallback logic - if (downloadLinks.length === 0) { - console.log(`[UHDMovies] Enhanced fallback failed, trying original fallback logic.`); - $('.entry-content').find('a[href*="tech.unblockedgames.world"], a[href*="tech.examzculture.in"]').each((i, el) => { - const linkElement = $(el); - const episodeRegex = new RegExp(`^Episode\\s+0*${episode}(?!\\d)`, 'i'); - - if (episodeRegex.test(linkElement.text().trim())) { - const link = linkElement.attr('href'); - if (link && !downloadLinks.some(item => item.link === link)) { - let qualityText = 'Unknown Quality'; - const parentP = linkElement.closest('p, div'); - const prevElement = parentP.prev(); - if (prevElement.length > 0) { - const prevText = prevElement.text().trim(); - if (prevText && prevText.length > 5 && !prevText.toLowerCase().includes('download')) { - qualityText = prevText; - } - } - - // Check if this episode belongs to the correct season - // Enhanced season check - look for various season formats - const seasonCheckRegexes = [ - new RegExp(`\\.S0*${season}[\\.]`, 'i'), // .S01. - new RegExp(`S0*${season}[\\.]`, 'i'), // S01. - new RegExp(`S0*${season}\\b`, 'i'), // S01 (word boundary) - new RegExp(`Season\\s+0*${season}\\b`, 'i'), // Season 1 - new RegExp(`S0*${season}`, 'i') // S01 anywhere - ]; - - const seasonMatch = seasonCheckRegexes.some(regex => regex.test(qualityText)); - if (!seasonMatch) { - console.log(`[UHDMovies] Skipping episode from different season: Quality='${qualityText}'`); - return; // Skip this episode as it's from a different season - } - - const sizeMatch = qualityText.match(/\[([0-9.,]+[KMGT]B[^\]]*)\]/i); - const size = sizeMatch ? sizeMatch[1] : 'Unknown'; - const cleanQuality = extractCleanQuality(qualityText); - const rawQuality = qualityText.replace(/(\r\n|\n|\r)/gm, " ").replace(/\s+/g, ' ').trim(); - - console.log(`[UHDMovies] Found match via original fallback: Quality='${qualityText}', Link='${link}'`); - downloadLinks.push({ quality: cleanQuality, size: size, link: link, rawQuality: rawQuality }); - } - } - }); - } - } - - if (downloadLinks.length > 0) { - console.log(`[UHDMovies] Found ${downloadLinks.length} links for S${season}E${episode}.`); - } else { - console.log(`[UHDMovies] Could not find links for S${season}E${episode}. It's possible the logic needs adjustment or the links aren't on the page.`); - } - - return { title: showTitle, links: downloadLinks }; - - } catch (error) { - console.error(`[UHDMovies] Error extracting TV show download links: ${error.message}`); - return { title: 'Unknown', links: [] }; - } -} - -// Function to extract download links from a movie page -async function extractDownloadLinks(moviePageUrl, targetYear = null) { - try { - console.log(`[UHDMovies] Extracting links from: ${moviePageUrl}`); - const response = await makeRequest(moviePageUrl); - const $ = cheerio.load(response.data); - - const movieTitle = $('h1').first().text().trim(); - const downloadLinks = []; - - // Find all download links (the new SID links) and their associated quality information - $('a[href*="tech.unblockedgames.world"], a[href*="tech.examzculture.in"]').each((index, element) => { - const link = $(element).attr('href'); - - if (link && !downloadLinks.some(item => item.link === link)) { - let quality = 'Unknown Quality'; - let size = 'Unknown'; - - // Method 1: Look for quality in the closest preceding paragraph or heading - const prevElement = $(element).closest('p').prev(); - if (prevElement.length > 0) { - const prevText = prevElement.text().trim(); - if (prevText && prevText.length > 20 && !prevText.includes('Download')) { - quality = prevText; - } - } - - // Method 2: Look for quality in parent's siblings - if (quality === 'Unknown Quality') { - const parentSiblings = $(element).parent().prevAll().first().text().trim(); - if (parentSiblings && parentSiblings.length > 20) { - quality = parentSiblings; - } - } - - // Method 3: Look for bold/strong text above the link - if (quality === 'Unknown Quality') { - const strongText = $(element).closest('p').prevAll().find('strong, b').last().text().trim(); - if (strongText && strongText.length > 20) { - quality = strongText; - } - } - - // Method 4: Look for the entire paragraph containing quality info - if (quality === 'Unknown Quality') { - let currentElement = $(element).parent(); - for (let i = 0; i < 5; i++) { - currentElement = currentElement.prev(); - if (currentElement.length === 0) break; - - const text = currentElement.text().trim(); - if (text && text.length > 30 && - (text.includes('1080p') || text.includes('720p') || text.includes('2160p') || - text.includes('4K') || text.includes('HEVC') || text.includes('x264') || text.includes('x265'))) { - quality = text; - break; - } - } - } - - // Year-based filtering for collections - if (targetYear && quality !== 'Unknown Quality') { - // Check for years in quality text - const yearMatches = quality.match(/\((\d{4})\)/g); - let hasMatchingYear = false; - - if (yearMatches && yearMatches.length > 0) { - for (const yearMatch of yearMatches) { - const year = parseInt(yearMatch.replace(/[()]/g, '')); - if (year === targetYear) { - hasMatchingYear = true; - break; - } - } - if (!hasMatchingYear) { - console.log(`[UHDMovies] Skipping link due to year mismatch. Target: ${targetYear}, Found: ${yearMatches.join(', ')} in "${quality}"`); - return; // Skip this link - } - } else { - // If no year in quality text, check filename and other indicators - const linkText = $(element).text().trim(); - const parentText = $(element).parent().text().trim(); - const combinedText = `${quality} ${linkText} ${parentText}`; - - // Look for years in combined text - const allYearMatches = combinedText.match(/\((\d{4})\)/g) || combinedText.match(/(\d{4})/g); - if (allYearMatches) { - let foundTargetYear = false; - for (const yearMatch of allYearMatches) { - const year = parseInt(yearMatch.replace(/[()]/g, '')); - if (year >= 1900 && year <= 2030) { // Valid movie year range - if (year === targetYear) { - foundTargetYear = true; - break; - } - } - } - if (!foundTargetYear && allYearMatches.length > 0) { - console.log(`[UHDMovies] Skipping link due to no matching year found. Target: ${targetYear}, Found years: ${allYearMatches.join(', ')} in combined text`); - return; // Skip this link - } - } - - // Additional check: if quality contains movie names that don't match target year - const lowerQuality = quality.toLowerCase(); - if (targetYear === 2015) { - if (lowerQuality.includes('wasp') || lowerQuality.includes('quantumania')) { - console.log(`[UHDMovies] Skipping link for 2015 target as it contains 'wasp' or 'quantumania': "${quality}"`); - return; // Skip this link - } - } - } - } - - // Extract size from quality text if present - const sizeMatch = quality.match(/\[([0-9.,]+\s*[KMGT]B[^\]]*)\]/); - if (sizeMatch) { - size = sizeMatch[1]; - } - - // Clean up the quality information - const cleanQuality = extractCleanQuality(quality); - - downloadLinks.push({ - quality: cleanQuality, - size: size, - link: link, - rawQuality: quality.replace(/(\r\n|\n|\r)/gm, " ").replace(/\s+/g, ' ').trim() - }); - } - }); - - return { - title: movieTitle, - links: downloadLinks - }; - - } catch (error) { - console.error(`[UHDMovies] Error extracting download links: ${error.message}`); - return { title: 'Unknown', links: [] }; - } -} - -function extractCodecs(rawQuality) { - const codecs = []; - const text = rawQuality.toLowerCase(); - - if (text.includes('hevc') || text.includes('x265')) { - codecs.push('H.265'); - } else if (text.includes('x264')) { - codecs.push('H.264'); - } - - if (text.includes('10bit') || text.includes('10-bit')) { - codecs.push('10-bit'); - } - - if (text.includes('atmos')) { - codecs.push('Atmos'); - } else if (text.includes('dts-hd')) { - codecs.push('DTS-HD'); - } else if (text.includes('dts')) { - codecs.push('DTS'); - } else if (text.includes('ddp5.1') || text.includes('dd+ 5.1') || text.includes('eac3')) { - codecs.push('EAC3'); - } else if (text.includes('ac3')) { - codecs.push('AC3'); - } - - if (text.includes('dovi') || text.includes('dolby vision') || /\bdv\b/.test(text)) { - codecs.push('DV'); - } else if (text.includes('hdr')) { - codecs.push('HDR'); - } - - return codecs; -} - -// Function to try Instant Download method -async function tryInstantDownload($) { - const instantDownloadLink = $('a:contains("Instant Download")').attr('href'); - if (!instantDownloadLink) { - return null; - } - - console.log('[UHDMovies] Found "Instant Download" link, attempting to extract final URL...'); - - try { - const urlParams = new URLSearchParams(new URL(instantDownloadLink).search); - const keys = urlParams.get('url'); - - if (keys) { - const apiUrl = `${new URL(instantDownloadLink).origin}/api`; - const formData = new FormData(); - formData.append('keys', keys); - - const apiResponse = await axiosInstance.post(apiUrl, formData, { - headers: { - ...formData.getHeaders(), - 'x-token': new URL(instantDownloadLink).hostname - } - }); - - if (apiResponse.data && apiResponse.data.url) { - let finalUrl = apiResponse.data.url; - // Fix spaces in workers.dev URLs by encoding them properly - if (finalUrl.includes('workers.dev')) { - const urlParts = finalUrl.split('/'); - const filename = urlParts[urlParts.length - 1]; - const encodedFilename = filename.replace(/ /g, '%20'); - urlParts[urlParts.length - 1] = encodedFilename; - finalUrl = urlParts.join('/'); - } - console.log('[UHDMovies] Extracted final link from API:', finalUrl); - return finalUrl; - } - } - - console.log('[UHDMovies] Could not find a valid final download link from Instant Download.'); - return null; - } catch (error) { - console.log(`[UHDMovies] Error processing "Instant Download": ${error.message}`); - return null; - } -} - -// Function to try Resume Cloud method -async function tryResumeCloud($) { - // Look for both "Resume Cloud" and "Cloud Resume Download" buttons - const resumeCloudButton = $('a:contains("Resume Cloud"), a:contains("Cloud Resume Download")'); - - if (resumeCloudButton.length === 0) { - return null; - } - - const resumeLink = resumeCloudButton.attr('href'); - if (!resumeLink) { - return null; - } - - // Check if it's already a direct download link (workers.dev) - if (resumeLink.includes('workers.dev') || resumeLink.startsWith('http')) { - let directLink = resumeLink; - // Fix spaces in workers.dev URLs by encoding them properly - if (directLink.includes('workers.dev')) { - const urlParts = directLink.split('/'); - const filename = urlParts[urlParts.length - 1]; - const encodedFilename = filename.replace(/ /g, '%20'); - urlParts[urlParts.length - 1] = encodedFilename; - directLink = urlParts.join('/'); - } - console.log(`[UHDMovies] Found direct "Cloud Resume Download" link: ${directLink}`); - return directLink; - } - - // Otherwise, follow the link to get the final download - try { - const resumeUrl = new URL(resumeLink, 'https://driveleech.net').href; - console.log(`[UHDMovies] Found 'Resume Cloud' page link. Following to: ${resumeUrl}`); - - // "Click" the link by making another request - const finalPageResponse = await makeRequest(resumeUrl, { maxRedirects: 10 }); - const $$ = cheerio.load(finalPageResponse.data); - - // Look for direct download links - let finalDownloadLink = $$('a.btn-success[href*="workers.dev"], a[href*="driveleech.net/d/"]').attr('href'); - - if (finalDownloadLink) { - // Fix spaces in workers.dev URLs by encoding them properly - if (finalDownloadLink.includes('workers.dev')) { - // Split the URL at the last slash to separate the base URL from the filename - const urlParts = finalDownloadLink.split('/'); - const filename = urlParts[urlParts.length - 1]; - // Encode spaces in the filename part only - const encodedFilename = filename.replace(/ /g, '%20'); - urlParts[urlParts.length - 1] = encodedFilename; - finalDownloadLink = urlParts.join('/'); - } - console.log(`[UHDMovies] Extracted final Resume Cloud link: ${finalDownloadLink}`); - return finalDownloadLink; - } else { - console.log('[UHDMovies] Could not find the final download link on the "Resume Cloud" page.'); - return null; - } - } catch (error) { - console.log(`[UHDMovies] Error processing "Resume Cloud": ${error.message}`); - return null; - } -} - -// Environment variable to control URL validation -const URL_VALIDATION_ENABLED = process.env.DISABLE_URL_VALIDATION !== 'true'; -console.log(`[UHDMovies] URL validation is ${URL_VALIDATION_ENABLED ? 'enabled' : 'disabled'}.`); - -// Validate if a video URL is working (not 404 or broken) -async function validateVideoUrl(url, timeout = 10000) { - // Skip validation if disabled via environment variable - if (!URL_VALIDATION_ENABLED) { - console.log(`[UHDMovies] URL validation disabled, skipping validation for: ${url.substring(0, 100)}...`); - return true; - } - - try { - console.log(`[UHDMovies] Validating URL: ${url.substring(0, 100)}...`); - - const response = await axiosInstance.head(url, { - timeout, - headers: { - 'Range': 'bytes=0-1' - } - }); - - // Check if status is OK (200-299) or partial content (206) - if (response.status >= 200 && response.status < 400) { - console.log(`[UHDMovies] ✓ URL validation successful (${response.status})`); - return true; - } else { - console.log(`[UHDMovies] ✗ URL validation failed with status: ${response.status}`); - return false; - } - } catch (error) { - console.log(`[UHDMovies] ✗ URL validation failed: ${error.message}`); - return false; - } -} - - -// Compare media to find matching result -function compareMedia(mediaInfo, searchResult) { - const normalizeString = (str) => String(str || '').toLowerCase().replace(/[^a-zA-Z0-9]/g, ''); - - const titleWithAnd = mediaInfo.title.replace(/\s*&\s*/g, ' and '); - const normalizedMediaTitle = normalizeString(titleWithAnd); - const normalizedResultTitle = normalizeString(searchResult.title); - - console.log(`[UHDMovies] Comparing: "${mediaInfo.title}" (${mediaInfo.year}) vs "${searchResult.title}"`); - console.log(`[UHDMovies] Normalized: "${normalizedMediaTitle}" vs "${normalizedResultTitle}"`); - - // Check if titles match or result title contains media title - let titleMatches = normalizedResultTitle.includes(normalizedMediaTitle); - - // If direct match fails, try checking for franchise/collection matches - if (!titleMatches) { - const mainTitle = normalizedMediaTitle.split('and')[0]; - const isCollection = normalizedResultTitle.includes('duology') || - normalizedResultTitle.includes('trilogy') || - normalizedResultTitle.includes('quadrilogy') || - normalizedResultTitle.includes('collection') || - normalizedResultTitle.includes('saga'); - - if (isCollection && normalizedResultTitle.includes(mainTitle)) { - console.log(`[UHDMovies] Found collection match: "${mainTitle}" in collection "${searchResult.title}"`); - titleMatches = true; - } - } - - if (!titleMatches) { - console.log(`[UHDMovies] Title mismatch: "${normalizedResultTitle}" does not contain "${normalizedMediaTitle}"`); - return false; - } - - // NEW: Negative keyword check for spinoffs - const negativeKeywords = ['challenge', 'conversation', 'story', 'in conversation']; - const originalTitleLower = mediaInfo.title.toLowerCase(); - for (const keyword of negativeKeywords) { - if (normalizedResultTitle.includes(keyword.replace(/\s/g, '')) && !originalTitleLower.includes(keyword)) { - console.log(`[UHDMovies] Rejecting spinoff due to keyword: "${keyword}"`); - return false; // It's a spinoff, reject it. - } - } - - // Check year if both are available - if (mediaInfo.year && searchResult.title) { - const yearRegex = /\b(19[89]\d|20\d{2})\b/g; // Look for years 1980-2099 - const yearMatchesInResult = searchResult.title.match(yearRegex); - const yearRangeMatch = searchResult.title.match(/\((\d{4})\s*-\s*(\d{4})\)/); - - let hasMatchingYear = false; - - if (yearMatchesInResult) { - console.log(`[UHDMovies] Found years in result: ${yearMatchesInResult.join(', ')}`); - if (yearMatchesInResult.some(yearStr => Math.abs(parseInt(yearStr) - mediaInfo.year) <= 1)) { - hasMatchingYear = true; - } - } - - if (!hasMatchingYear && yearRangeMatch) { - console.log(`[UHDMovies] Found year range in result: ${yearRangeMatch[0]}`); - const startYear = parseInt(yearRangeMatch[1]); - const endYear = parseInt(yearRangeMatch[2]); - if (mediaInfo.year >= startYear - 1 && mediaInfo.year <= endYear + 1) { - hasMatchingYear = true; - } - } - - // If there are any years found in the title, one of them MUST match. - if ((yearMatchesInResult || yearRangeMatch) && !hasMatchingYear) { - console.log(`[UHDMovies] Year mismatch. Target: ${mediaInfo.year}, but no matching year found in result.`); - return false; - } - } - - console.log(`[UHDMovies] Match successful!`); - return true; -} - -// Function to score search results based on quality keywords and season coverage -function scoreResult(title, requestedSeason = null) { - let score = 0; - const lowerTitle = title.toLowerCase(); - - // Quality scoring - if (lowerTitle.includes('remux')) score += 10; - if (lowerTitle.includes('bluray') || lowerTitle.includes('blu-ray')) score += 8; - if (lowerTitle.includes('imax')) score += 6; - if (lowerTitle.includes('4k') || lowerTitle.includes('2160p')) score += 5; - if (lowerTitle.includes('dovi') || lowerTitle.includes('dolby vision') || /\\bdv\\b/.test(lowerTitle)) score += 4; - if (lowerTitle.includes('hdr')) score += 3; - if (lowerTitle.includes('1080p')) score += 2; - if (lowerTitle.includes('hevc') || lowerTitle.includes('x265')) score += 1; - - // Season coverage scoring (for TV shows) - if (requestedSeason !== null) { - // Check for season range formats like "Season 1 – 2" or "Season 1-2" - const seasonRangeMatch = lowerTitle.match(/season\s+(\d+)\s*[–-]\s*(\d+)/i); - if (seasonRangeMatch) { - const startSeason = parseInt(seasonRangeMatch[1], 10); - const endSeason = parseInt(seasonRangeMatch[2], 10); - if (requestedSeason >= startSeason && requestedSeason <= endSeason) { - score += 50; // High bonus for season range that includes requested season - console.log(`[UHDMovies] Season range bonus (+50): ${startSeason}-${endSeason} includes requested season ${requestedSeason}`); - } - } - - // Check for specific season mentions - const specificSeasonMatch = lowerTitle.match(/season\s+(\d+)/i); - if (specificSeasonMatch) { - const mentionedSeason = parseInt(specificSeasonMatch[1], 10); - if (mentionedSeason === requestedSeason) { - score += 30; // Good bonus for exact season match - console.log(`[UHDMovies] Exact season bonus (+30): Season ${mentionedSeason} matches requested season ${requestedSeason}`); - } else if (mentionedSeason < requestedSeason) { - score -= 20; // Penalty for older season when requesting newer season - console.log(`[UHDMovies] Season penalty (-20): Season ${mentionedSeason} is older than requested season ${requestedSeason}`); - } - } - - // Check for "Season X Added" or similar indicators - if (lowerTitle.includes('season') && lowerTitle.includes('added')) { - const addedSeasonMatch = lowerTitle.match(/season\s+(\d+)\s+added/i); - if (addedSeasonMatch) { - const addedSeason = parseInt(addedSeasonMatch[1], 10); - if (addedSeason === requestedSeason) { - score += 40; // High bonus for newly added season - console.log(`[UHDMovies] Added season bonus (+40): Season ${addedSeason} was recently added`); - } - } - } - } - - return score; -} - -// Function to parse size string into MB -function parseSize(sizeString) { - if (!sizeString || typeof sizeString !== 'string') { - return 0; - } - - const upperCaseSizeString = sizeString.toUpperCase(); - - // Regex to find a number (integer or float) followed by GB, MB, or KB - const match = upperCaseSizeString.match(/([0-9.,]+)\s*(GB|MB|KB)/); - - if (!match) { - return 0; - } - - const sizeValue = parseFloat(match[1].replace(/,/g, '')); - if (isNaN(sizeValue)) { - return 0; - } - - const unit = match[2]; - - if (unit === 'GB') { - return sizeValue * 1024; - } else if (unit === 'MB') { - return sizeValue; - } else if (unit === 'KB') { - return sizeValue / 1024; - } - - return 0; -} - -// Helper function to extract cookies from jar for a specific URL -const getCookiesForUrl = async (jar, url) => { - try { - const cookies = await jar.getCookies(url); - if (cookies && cookies.length > 0) { - return cookies.map(cookie => cookie.toString()).join('; '); - } - } catch (error) { - console.log(`[UHDMovies] Error extracting cookies for ${url}: ${error.message}`); - } - return null; -}; - -// Helper function to create a session for SID resolution -const createSession = async (jar) => { - const { wrapper } = await getAxiosCookieJarSupport(); - - const sessionConfig = { - jar, - headers: { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36', - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', - 'Accept-Language': 'en-US,en;q=0.5', - 'Connection': 'keep-alive', - 'Upgrade-Insecure-Requests': '1' - } - }; - - const session = wrapper(axios.create(sessionConfig)); - - - - return session; -}; - -// New function to resolve the tech.unblockedgames.world links -async function resolveSidToDriveleech(sidUrl) { - console.log(`[UHDMovies] Resolving SID link: ${sidUrl}`); - const { origin } = new URL(sidUrl); - const jar = new CookieJar(); - - // Create session - const session = await createSession(jar); - - try { - // Step 0: Get the _wp_http value - console.log(" [SID] Step 0: Fetching initial page..."); - const responseStep0 = await session.get(sidUrl); - let $ = cheerio.load(responseStep0.data); - const initialForm = $('#landing'); - const wp_http_step1 = initialForm.find('input[name="_wp_http"]').val(); - const action_url_step1 = initialForm.attr('action'); - - if (!wp_http_step1 || !action_url_step1) { - console.error(" [SID] Error: Could not find _wp_http in initial form."); - return null; - } - - // Step 1: POST to the first form's action URL - console.log(" [SID] Step 1: Submitting initial form..."); - const step1Data = new URLSearchParams({ '_wp_http': wp_http_step1 }); - const responseStep1 = await session.post(action_url_step1, step1Data, { - headers: { 'Referer': sidUrl, 'Content-Type': 'application/x-www-form-urlencoded' } - }); - - // Step 2: Parse verification page for second form - console.log(" [SID] Step 2: Parsing verification page..."); - $ = cheerio.load(responseStep1.data); - const verificationForm = $('#landing'); - const action_url_step2 = verificationForm.attr('action'); - const wp_http2 = verificationForm.find('input[name="_wp_http2"]').val(); - const token = verificationForm.find('input[name="token"]').val(); - - if (!action_url_step2) { - console.error(" [SID] Error: Could not find verification form."); - return null; - } - - // Step 3: POST to the verification URL - console.log(" [SID] Step 3: Submitting verification..."); - const step2Data = new URLSearchParams({ '_wp_http2': wp_http2, 'token': token }); - const responseStep2 = await session.post(action_url_step2, step2Data, { - headers: { 'Referer': responseStep1.request.res.responseUrl, 'Content-Type': 'application/x-www-form-urlencoded' } - }); - - // Step 4: Find dynamic cookie and link from JavaScript - console.log(" [SID] Step 4: Parsing final page for JS data..."); - let finalLinkPath = null; - let cookieName = null; - let cookieValue = null; - - const scriptContent = responseStep2.data; - const cookieMatch = scriptContent.match(/s_343\('([^']+)',\s*'([^']+)'/); - const linkMatch = scriptContent.match(/c\.setAttribute\("href",\s*"([^"]+)"\)/); - - if (cookieMatch) { - cookieName = cookieMatch[1].trim(); - cookieValue = cookieMatch[2].trim(); - } - if (linkMatch) { - finalLinkPath = linkMatch[1].trim(); - } - - if (!finalLinkPath || !cookieName || !cookieValue) { - console.error(" [SID] Error: Could not extract dynamic cookie/link from JS."); - return null; - } - - const finalUrl = new URL(finalLinkPath, origin).href; - console.log(` [SID] Dynamic link found: ${finalUrl}`); - console.log(` [SID] Dynamic cookie found: ${cookieName}`); - - // Step 5: Set cookie and make final request - console.log(" [SID] Step 5: Setting cookie and making final request..."); - await jar.setCookie(`${cookieName}=${cookieValue}`, origin); - - const finalResponse = await session.get(finalUrl, { - headers: { 'Referer': responseStep2.request.res.responseUrl } - }); - - // Step 6: Extract driveleech URL from meta refresh tag - $ = cheerio.load(finalResponse.data); - const metaRefresh = $('meta[http-equiv="refresh"]'); - if (metaRefresh.length > 0) { - const content = metaRefresh.attr('content'); - const urlMatch = content.match(/url=(.*)/i); - if (urlMatch && urlMatch[1]) { - const driveleechUrl = urlMatch[1].replace(/"/g, "").replace(/'/g, ""); - console.log(` [SID] SUCCESS! Resolved Driveleech URL: ${driveleechUrl}`); - return driveleechUrl; - } - } - - console.error(" [SID] Error: Could not find meta refresh tag with Driveleech URL."); - return null; - - } catch (error) { - console.error(` [SID] Error during SID resolution: ${error.message}`); - if (error.response) { - console.error(` [SID] Status: ${error.response.status}`); - } - return null; - } -} - -// Main function to get streams for TMDB content -async function getUHDMoviesStreams(tmdbId, mediaType = 'movie', season = null, episode = null) { - console.log(`[UHDMovies] Attempting to fetch streams for TMDB ID: ${tmdbId}, Type: ${mediaType}${mediaType === 'tv' ? `, S:${season}E:${episode}` : ''}`); - - const cacheKey = `uhd_final_v18_${tmdbId}_${mediaType}${season ? `_s${season}e${episode}` : ''}`; - - try { - // 1. Check cache first - let cachedLinks = await getFromCache(cacheKey); - if (cachedLinks && cachedLinks.length > 0) { - console.log(`[UHDMovies] Cache HIT for ${cacheKey}. Using ${cachedLinks.length} cached Driveleech links.`); - } else { - if (cachedLinks && cachedLinks.length === 0) { - console.log(`[UHDMovies] Cache contains empty data for ${cacheKey}. Refetching from source.`); - } else { - console.log(`[UHDMovies] Cache MISS for ${cacheKey}. Fetching from source.`); - } - console.log(`[UHDMovies] Cache MISS for ${cacheKey}. Fetching from source.`); - // 2. If cache miss, get TMDB info to perform search - const TMDB_API_KEY_UHDMOVIES = getUhdMoviesKey(); - const tmdbUrl = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY_UHDMOVIES}`; - const tmdbResponse = await axios.get(tmdbUrl); - const tmdbData = tmdbResponse.data; - const mediaInfo = { - title: mediaType === 'tv' ? tmdbData.name : tmdbData.title, - year: parseInt(((mediaType === 'tv' ? tmdbData.first_air_date : tmdbData.release_date) || '').split('-')[0], 10) - }; - - if (!mediaInfo.title) throw new Error('Could not extract title from TMDB response.'); - console.log(`[UHDMovies] TMDB Info: "${mediaInfo.title}" (${mediaInfo.year || 'N/A'})`); - - // 3. Search for the media on UHDMovies - const searchTitle = mediaInfo.title.replace(/:/g, '').replace(/\s*&\s*/g, ' and '); - let searchResults = await searchMovies(searchTitle); - - // If no results or only wrong year results, try fallback search with just main title - if (searchResults.length === 0 || !searchResults.some(result => compareMedia(mediaInfo, result))) { - console.log(`[UHDMovies] Primary search failed or no matches. Trying fallback search...`); - - // Extract main title (remove subtitles after colon, "and the", etc.) - let fallbackTitle = mediaInfo.title.split(':')[0].trim(); - if (fallbackTitle.includes('and the')) { - fallbackTitle = fallbackTitle.split('and the')[0].trim(); - } - if (fallbackTitle !== searchTitle) { - console.log(`[UHDMovies] Fallback search with: "${fallbackTitle}"`); - const fallbackResults = await searchMovies(fallbackTitle); - if (fallbackResults.length > 0) { - searchResults = fallbackResults; - } - } - } - - if (searchResults.length === 0) { - console.log(`[UHDMovies] No search results found for "${mediaInfo.title}".`); - // Don't cache empty results to allow retrying later - return []; - } - - // 4. Find the best matching result - const matchingResults = searchResults.filter(result => compareMedia(mediaInfo, result)); - - if (matchingResults.length === 0) { - console.log(`[UHDMovies] No matching content found for "${mediaInfo.title}" (${mediaInfo.year}).`); - // Don't cache empty results to allow retrying later - return []; - } - - let matchingResult; - let scoredResults = null; // Declare outside the conditional - - if (matchingResults.length === 1) { - matchingResult = matchingResults[0]; - } else { - console.log(`[UHDMovies] Found ${matchingResults.length} matching results. Scoring to find the best...`); - - scoredResults = matchingResults.map(result => { - const score = scoreResult(result.title, mediaType === 'tv' ? season : null); - console.log(` - Score ${score}: ${result.title}`); - return { ...result, score }; - }).sort((a, b) => b.score - a.score); - - matchingResult = scoredResults[0]; - console.log(`[UHDMovies] Best match selected with score ${matchingResult.score}: "${matchingResult.title}"`); - } - - console.log(`[UHDMovies] Found matching content: "${matchingResult.title}"`); - - // 5. Extract SID links from the movie/show page - let downloadInfo = await (mediaType === 'tv' ? extractTvShowDownloadLinks(matchingResult.link, season, episode) : extractDownloadLinks(matchingResult.link, mediaInfo.year)); - - // Check if season was not found or episode extraction failed, and we have multiple results to try - if (downloadInfo.links.length === 0 && matchingResults.length > 1 && scoredResults && - (downloadInfo.seasonNotFound || (mediaType === 'tv' && downloadInfo.title))) { - console.log(`[UHDMovies] Season ${season} not found or episode extraction failed on best match. Trying next best match...`); - - // Try the next best match - const nextBestMatch = scoredResults[1]; - console.log(`[UHDMovies] Trying next best match: "${nextBestMatch.title}"`); - - downloadInfo = await (mediaType === 'tv' ? extractTvShowDownloadLinks(nextBestMatch.link, season, episode) : extractDownloadLinks(nextBestMatch.link, mediaInfo.year)); - - if (downloadInfo.links.length > 0) { - console.log(`[UHDMovies] Successfully found links on next best match!`); - } else { - console.log(`[UHDMovies] Next best match also failed. No download links found.`); - } - } - - if (downloadInfo.links.length === 0) { - console.log('[UHDMovies] No download links found on page.'); - // Don't cache empty results to allow retrying later - return []; - } - - // 6. Resolve all SID links to driveleech redirect URLs (intermediate step) - console.log(`[UHDMovies] Resolving ${downloadInfo.links.length} SID link(s) to driveleech redirect URLs...`); - const resolutionPromises = downloadInfo.links.map(async (linkInfo) => { - try { - let driveleechUrl = null; - - if (linkInfo.link && (linkInfo.link.includes('tech.unblockedgames.world') || linkInfo.link.includes('tech.creativeexpressionsblog.com') || linkInfo.link.includes('tech.examzculture.in'))) { - driveleechUrl = await resolveSidToDriveleech(linkInfo.link); - } else if (linkInfo.link && (linkInfo.link.includes('driveseed.org') || linkInfo.link.includes('driveleech.net'))) { - // If it's already a direct driveseed/driveleech link, use it - driveleechUrl = linkInfo.link; - } - - if (!driveleechUrl) return null; - - console.log(`[UHDMovies] Caching driveleech redirect URL for ${linkInfo.quality}: ${driveleechUrl}`); - return { ...linkInfo, driveleechRedirectUrl: driveleechUrl }; - } catch (error) { - console.error(`[UHDMovies] Error resolving ${linkInfo.quality}: ${error.message}`); - return null; - } - }); - - cachedLinks = (await Promise.all(resolutionPromises)).filter(Boolean); - - // 7. Save the successfully resolved driveleech redirect URLs to the cache - if (cachedLinks.length > 0) { - console.log(`[UHDMovies] Caching ${cachedLinks.length} resolved driveleech redirect URLs for key: ${cacheKey}`); - await saveToCache(cacheKey, cachedLinks); - } else { - console.log(`[UHDMovies] No driveleech redirect URLs could be resolved. Not caching to allow retrying later.`); - return []; - } - } - - if (!cachedLinks || cachedLinks.length === 0) { - console.log('[UHDMovies] No final file page URLs found after scraping/cache check.'); - return []; - } - - // 8. Process all cached driveleech redirect URLs to get streaming links - console.log(`[UHDMovies] Processing ${cachedLinks.length} cached driveleech redirect URL(s) to get streaming links.`); - const streamPromises = cachedLinks.map(async (linkInfo) => { - try { - // First, resolve the driveleech redirect URL to get the final file page URL - const response = await makeRequest(linkInfo.driveleechRedirectUrl, { maxRedirects: 10 }); - let $ = cheerio.load(response.data); - - // Check for JavaScript redirect (window.location.replace) - const scriptContent = $('script').html(); - const redirectMatch = scriptContent && scriptContent.match(/window\.location\.replace\("([^"]+)"\)/); - - let finalFilePageUrl = linkInfo.driveleechRedirectUrl; - if (redirectMatch && redirectMatch[1]) { - finalFilePageUrl = new URL(redirectMatch[1], 'https://driveleech.net/').href; - console.log(`[UHDMovies] Resolved redirect to final file page: ${finalFilePageUrl}`); - - // Load the final file page - const finalResponse = await makeRequest(finalFilePageUrl, { maxRedirects: 10 }); - $ = cheerio.load(finalResponse.data); - } - - // Extract file size and name information - let sizeInfo = 'Unknown'; - let fileName = null; - - const sizeElement = $('li.list-group-item:contains("Size :")').text(); - if (sizeElement) { - const sizeMatch = sizeElement.match(/Size\s*:\s*([0-9.,]+\s*[KMGT]B)/); - if (sizeMatch) { - sizeInfo = sizeMatch[1]; - } - } - - const nameElement = $('li.list-group-item:contains("Name :")'); - if (nameElement.length > 0) { - fileName = nameElement.text().replace('Name :', '').trim(); - } else { - const h5Title = $('div.card-header h5').clone().children().remove().end().text().trim(); - if (h5Title) { - fileName = h5Title.replace(/\[.*\]/, '').trim(); - } - } - - // Try download methods to get final streaming URL - const downloadMethods = [ - { name: 'Resume Cloud', func: tryResumeCloud }, - { name: 'Instant Download', func: tryInstantDownload } - ]; - - for (const method of downloadMethods) { - try { - const finalUrl = await method.func($); - - if (finalUrl) { - const isValid = await validateVideoUrl(finalUrl); - if (isValid) { - const rawQuality = linkInfo.rawQuality || ''; - const codecs = extractCodecs(rawQuality); - const cleanFileName = fileName ? fileName.replace(/\.[^/.]+$/, "").replace(/[._]/g, ' ') : (linkInfo.quality || 'Unknown'); - - return { - name: `UHDMovies`, - title: `${cleanFileName}\n${sizeInfo}`, - url: finalUrl, - quality: linkInfo.quality, - provider: 'uhdmovies', - headers: {} - }; - } - } - } catch (error) { - console.log(`[UHDMovies] ${method.name} failed: ${error.message}`); - } - } - - return null; - } catch (error) { - console.error(`[UHDMovies] Error processing cached driveleech redirect ${linkInfo.driveleechRedirectUrl}: ${error.message}`); - return null; - } - }); - - const streams = (await Promise.all(streamPromises)).filter(Boolean); - console.log(`[UHDMovies] Successfully processed ${streams.length} final stream links.`); - - // Sort final streams by size - streams.sort((a, b) => { - const sizeA = parseSize(a.size); - const sizeB = parseSize(b.size); - return sizeB - sizeA; - }); - - return streams; - } catch (error) { - console.error(`[UHDMovies] A critical error occurred in getUHDMoviesStreams for ${tmdbId}: ${error.message}`); - if (error.stack) console.error(error.stack); - return []; - } -} - -module.exports = { getUHDMoviesStreams }; \ No newline at end of file diff --git a/providers/videasy.js b/providers/videasy.js new file mode 100644 index 0000000..c36a26f --- /dev/null +++ b/providers/videasy.js @@ -0,0 +1,116 @@ +const axios = require('axios'); +const { getTmdbApiKey } = require('../utils/tmdbKey'); + +const VIDEASY_HEADERS = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36', + 'Accept': 'application/json, text/plain, */*', + 'Origin': 'https://player.videasy.net', + 'Referer': 'https://player.videasy.net/' +}; + +// Each server maps a name to its API URL. moviesOnly:true skips TV requests. +const SERVERS = { + 'Neon': { url: 'https://api.videasy.net/myflixerzupcloud/sources-with-title' }, + 'Yoru': { url: 'https://api.videasy.net/cdn/sources-with-title', moviesOnly: true }, + 'Cypher': { url: 'https://api.videasy.net/moviebox/sources-with-title' }, + 'Reyna': { url: 'https://api.videasy.net/primewire/sources-with-title' }, + 'Omen': { url: 'https://api.videasy.net/onionplay/sources-with-title' }, + 'Breach': { url: 'https://api.videasy.net/m4uhd/sources-with-title' }, + 'Ghost': { url: 'https://api.videasy.net/primesrcme/sources-with-title' }, + 'Sage': { url: 'https://api.videasy.net/1movies/sources-with-title' }, + 'Vyse': { url: 'https://api.videasy.net/hdmovie/sources-with-title' }, + 'Raze': { url: 'https://api.videasy.net/superflix/sources-with-title' } +}; + +const DECRYPT_URL = 'https://enc-dec.app/api/dec-videasy'; + +async function getVideasyStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) { + console.log(`[Videasy] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`); + + const tmdbKey = getTmdbApiKey(); + if (!tmdbKey) { + console.error('[Videasy] No TMDB API key configured.'); + return []; + } + + // Step 1: Resolve title/year/imdbId from TMDB + let details; + try { + const type = mediaType === 'tv' ? 'tv' : 'movie'; + const { data } = await axios.get( + `https://api.themoviedb.org/3/${type}/${tmdbId}?api_key=${tmdbKey}&append_to_response=external_ids`, + { timeout: 8000 } + ); + details = { + title: data.title || data.name || '', + year: (data.release_date || data.first_air_date || '').split('-')[0], + imdbId: (data.external_ids && data.external_ids.imdb_id) || '', + type + }; + } catch (err) { + console.error(`[Videasy] TMDB lookup failed: ${err.message}`); + return []; + } + + if (!details.title) { + console.error('[Videasy] No title returned from TMDB.'); + return []; + } + + const allStreams = []; + const seen = new Set(); + + // Step 2: Query each server in parallel + await Promise.all(Object.entries(SERVERS).map(async ([name, server]) => { + if (server.moviesOnly && mediaType === 'tv') return; + + let apiUrl = `${server.url}?title=${encodeURIComponent(details.title)}` + + `&mediaType=${details.type}&year=${details.year}` + + `&tmdbId=${tmdbId}&imdbId=${details.imdbId || ''}`; + if (mediaType === 'tv') apiUrl += `&seasonId=${seasonNum}&episodeId=${episodeNum}`; + + try { + const encRes = await axios.get(apiUrl, { + headers: VIDEASY_HEADERS, + timeout: 8000, + responseType: 'text' + }); + + const encryptedText = typeof encRes.data === 'string' ? encRes.data : JSON.stringify(encRes.data); + if (!encryptedText || encryptedText.length < 20 || encryptedText.startsWith('<')) return; + + // Step 3: Decrypt via enc-dec.app + const decRes = await axios.post(DECRYPT_URL, + { text: encryptedText, id: String(tmdbId) }, + { headers: { 'Content-Type': 'application/json' }, timeout: 8000 } + ); + + const resData = (decRes.data && decRes.data.result) || decRes.data; + if (!resData || !Array.isArray(resData.sources)) return; + + for (const s of resData.sources) { + if (!s.url || seen.has(s.url)) continue; + seen.add(s.url); + allStreams.push({ + name: `Videasy ${name}`, + title: `Videasy ${name} - ${s.quality || 'Auto'}`, + url: s.url, + quality: s.quality || 'Auto', + provider: 'Videasy', + headers: { + 'Referer': 'https://player.videasy.net/', + 'Origin': 'https://player.videasy.net' + } + }); + } + console.log(`[Videasy] Server ${name}: ${resData.sources.length} source(s)`); + } catch { + // server unreachable or returned no data — skip silently + } + })); + + console.log(`[Videasy] Total streams: ${allStreams.length}`); + return allStreams; +} + +module.exports = { getVideasyStreams }; diff --git a/providers/vidlink.js b/providers/vidlink.js new file mode 100644 index 0000000..352bfe8 --- /dev/null +++ b/providers/vidlink.js @@ -0,0 +1,51 @@ +const axios = require('axios'); + +const VIDLINK_HEADERS = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36', + 'Referer': 'https://vidlink.pro' +}; + +async function getVidlinkStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) { + console.log(`[Vidlink] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`); + + try { + // Step 1: Encrypt the TMDB ID via enc-dec.app + const encRes = await axios.get( + `https://enc-dec.app/api/enc-vidlink?text=${encodeURIComponent(String(tmdbId))}`, + { timeout: 8000 } + ); + const encodedTmdb = encRes.data && encRes.data.result; + if (!encodedTmdb) { + console.log('[Vidlink] Encryption step returned no result.'); + return []; + } + + // Step 2: Fetch stream playlist from Vidlink API + const apiUrl = mediaType === 'tv' + ? `https://vidlink.pro/api/b/tv/${encodedTmdb}/${seasonNum}/${episodeNum}?multiLang=0` + : `https://vidlink.pro/api/b/movie/${encodedTmdb}?multiLang=0`; + + const apiRes = await axios.get(apiUrl, { headers: VIDLINK_HEADERS, timeout: 8000 }); + + const playlist = apiRes.data && apiRes.data.stream && apiRes.data.stream.playlist; + if (!playlist) { + console.log('[Vidlink] No playlist URL in response.'); + return []; + } + + console.log(`[Vidlink] Got stream.`); + return [{ + name: 'Vidlink', + title: 'Vidlink', + url: playlist, + quality: 'Auto', + provider: 'Vidlink', + headers: { 'Referer': 'https://vidlink.pro' } + }]; + } catch (err) { + console.error(`[Vidlink] Error: ${err.message}`); + return []; + } +} + +module.exports = { getVidlinkStreams }; diff --git a/providers/vidsrcextractor.js b/providers/vidsrcextractor.js deleted file mode 100644 index 81473f3..0000000 --- a/providers/vidsrcextractor.js +++ /dev/null @@ -1,411 +0,0 @@ -#!/usr/bin/env node -const cheerio = require('cheerio'); -// --- Constants --- -let BASEDOM = "https://cloudnestra.com"; // This can be updated by serversLoad -const SOURCE_URL = "https://vidsrc.xyz/embed"; -// --- Helper: Direct Fetch --- -async function fetchWrapper(url, options) { - console.log(`[VidSrc Direct] Fetching: ${url}`); - return fetch(url, options); -} -// --- Helper Functions (copied and adapted from src/extractor.ts) --- -async function serversLoad(html) { - const $ = cheerio.load(html); - const servers = []; - const title = $("title").text() ?? ""; - const baseFrameSrc = $("iframe").attr("src") ?? ""; - if (baseFrameSrc) { - try { - const fullUrl = baseFrameSrc.startsWith("//") ? "https:" + baseFrameSrc : baseFrameSrc; - BASEDOM = new URL(fullUrl).origin; - } - catch (e) { - console.warn(`(Attempt 1) Failed to parse base URL from iframe src: ${baseFrameSrc} using new URL(), error: ${e.message}`); - // Attempt 2: Regex fallback for origin - const originMatch = (baseFrameSrc.startsWith("//") ? "https:" + baseFrameSrc : baseFrameSrc).match(/^(https?:\/\/[^/]+)/); - if (originMatch && originMatch[1]) { - BASEDOM = originMatch[1]; - console.log(`(Attempt 2) Successfully extracted origin using regex: ${BASEDOM}`); - } else { - console.error(`(Attempt 2) Failed to extract origin using regex from: ${baseFrameSrc}. Using default: ${BASEDOM}`); - // Keep the default BASEDOM = "https://cloudnestra.com" if all fails - } - } - } - $(".serversList .server").each((index, element) => { - const server = $(element); - servers.push({ - name: server.text().trim(), - dataHash: server.attr("data-hash") ?? null, - }); - }); - return { - servers: servers, - title: title, - }; -} -async function parseMasterM3U8(m3u8Content, masterM3U8Url) { - const lines = m3u8Content.split('\n').map(line => line.trim()); - const streams = []; - for (let i = 0; i < lines.length; i++) { - if (lines[i].startsWith("#EXT-X-STREAM-INF:")) { - const infoLine = lines[i]; - let quality = "unknown"; - const resolutionMatch = infoLine.match(/RESOLUTION=(\d+x\d+)/); - if (resolutionMatch && resolutionMatch[1]) { - quality = resolutionMatch[1]; - } - else { - const bandwidthMatch = infoLine.match(/BANDWIDTH=(\d+)/); - if (bandwidthMatch && bandwidthMatch[1]) { - quality = `${Math.round(parseInt(bandwidthMatch[1]) / 1000)}kbps`; - } - } - if (i + 1 < lines.length && lines[i + 1] && !lines[i + 1].startsWith("#")) { - const streamUrlPart = lines[i + 1]; - try { - const fullStreamUrl = new URL(streamUrlPart, masterM3U8Url).href; - streams.push({ quality: quality, url: fullStreamUrl }); - } - catch (e) { - console.error(`Error constructing URL for stream part: ${streamUrlPart} with base: ${masterM3U8Url}`, e); - streams.push({ quality: quality, url: streamUrlPart }); // Store partial URL as a fallback - } - i++; - } - } - } - - // Sort streams by quality (highest first) - streams.sort((a, b) => { - // Extract resolution height from quality (e.g., "1280x720" -> 720) - const getHeight = (quality) => { - const match = quality.match(/(\d+)x(\d+)/); - return match ? parseInt(match[2], 10) : 0; - }; - - const heightA = getHeight(a.quality); - const heightB = getHeight(b.quality); - - // Higher resolution comes first - return heightB - heightA; - }); - - return streams; -} -async function PRORCPhandler(prorcp) { - try { - const prorcpUrl = `${BASEDOM}/prorcp/${prorcp}`; - const prorcpFetch = await fetchWrapper(prorcpUrl, { - headers: { - "accept": "*/*", "accept-language": "en-US,en;q=0.9", "priority": "u=1", - "sec-ch-ua": "\"Chromium\";v=\"128\", \"Not;A=Brand\";v=\"24\", \"Google Chrome\";v=\"128\"", - "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": "\"Windows\"", - "sec-fetch-dest": "script", "sec-fetch-mode": "no-cors", "sec-fetch-site": "same-origin", - 'Sec-Fetch-Dest': 'iframe', "Referer": `${BASEDOM}/`, "Referrer-Policy": "origin", - }, - timeout: 10000 - }); - if (!prorcpFetch.ok) { - console.error(`Failed to fetch prorcp: ${prorcpUrl}, status: ${prorcpFetch.status}`); - return null; - } - const prorcpResponse = await prorcpFetch.text(); - const regex = /file:\s*'([^']*)'/gm; - const match = regex.exec(prorcpResponse); - if (match && match[1]) { - const masterM3U8Url = match[1]; - const m3u8FileFetch = await fetchWrapper(masterM3U8Url, { - headers: { "Referer": prorcpUrl, "Accept": "*/*" }, - timeout: 10000 - }); - if (!m3u8FileFetch.ok) { - console.error(`Failed to fetch master M3U8: ${masterM3U8Url}, status: ${m3u8FileFetch.status}`); - return null; - } - const m3u8Content = await m3u8FileFetch.text(); - return parseMasterM3U8(m3u8Content, masterM3U8Url); - } - console.warn("No master M3U8 URL found in prorcp response for:", prorcpUrl); - return null; - } - catch (error) { - console.error(`Error in PRORCPhandler for ${BASEDOM}/prorcp/${prorcp}:`, error); - return null; - } -} -async function SRCRCPhandler(srcrcpPath, refererForSrcrcp) { - try { - const srcrcpUrl = BASEDOM + srcrcpPath; - console.log(`[VidSrc - SRCRCP] Fetching: ${srcrcpUrl} (Referer: ${refererForSrcrcp})`); - const response = await fetchWrapper(srcrcpUrl, { - headers: { - "accept": "*/*", - "accept-language": "en-US,en;q=0.9", - "sec-ch-ua": "\"Chromium\";v=\"128\", \"Not;A=Brand\";v=\"24\", \"Google Chrome\";v=\"128\"", - "sec-ch-ua-mobile": "?0", - "sec-ch-ua-platform": "\"Windows\"", - "sec-fetch-dest": "iframe", - "sec-fetch-mode": "navigate", - "sec-fetch-site": "same-origin", // This might need to be 'cross-site' if BASEDOM's origin differs from srcrcp link's origin - "Referer": refererForSrcrcp, - "Referrer-Policy": "origin", - }, - timeout: 10000 - }); - - if (!response.ok) { - console.error(`[VidSrc - SRCRCP] Failed to fetch ${srcrcpUrl}, status: ${response.status}`); - return null; - } - - const responseText = await response.text(); - console.log(`[VidSrc - SRCRCP] Response from ${srcrcpUrl} (first 500 chars): ${responseText.substring(0, 500)}`); - - // Attempt 1: Check for "file: '...'" like in PRORCP - const fileRegex = /file:\s*'([^']*)'/gm; - const fileMatch = fileRegex.exec(responseText); - if (fileMatch && fileMatch[1]) { - const masterM3U8Url = fileMatch[1]; - console.log(`[VidSrc - SRCRCP] Found M3U8 URL (via fileMatch): ${masterM3U8Url}`); - const m3u8FileFetch = await fetchWrapper(masterM3U8Url, { - headers: { "Referer": srcrcpUrl, "Accept": "*/*" }, - timeout: 10000 - }); - if (!m3u8FileFetch.ok) { - console.error(`[VidSrc - SRCRCP] Failed to fetch master M3U8: ${masterM3U8Url}, status: ${m3u8FileFetch.status}`); - return null; - } - const m3u8Content = await m3u8FileFetch.text(); - return parseMasterM3U8(m3u8Content, masterM3U8Url); - } - - // Attempt 2: Check if the responseText itself is an M3U8 playlist - if (responseText.trim().startsWith("#EXTM3U")) { - console.log(`[VidSrc - SRCRCP] Response from ${srcrcpUrl} appears to be an M3U8 playlist directly.`); - return parseMasterM3U8(responseText, srcrcpUrl); - } - - // Attempt 3: Look for sources = [...] or sources: [...] in script tags or in JSON-like structures - const $ = cheerio.load(responseText); - let sourcesFound = null; - $('script').each((i, script) => { - const scriptContent = $(script).html(); - if (scriptContent) { - // Regex for various ways sources might be defined - const sourcesRegexes = [ - /sources\s*[:=]\s*(\[[^\]]*\{(?:\s*|.*?)file\s*:\s*['"]([^'"]+)['"](?:\s*|.*?)\}[^\]]*\])/si, // extracts the URL from sources: [{file: "URL"}] - /playerInstance\.setup\s*\(\s*\{\s*sources\s*:\s*(\[[^\]]*\{(?:\s*|.*?)file\s*:\s*['"]([^'"]+)['"](?:\s*|.*?)\}[^\]]*\])/si, // for playerInstance.setup({sources: [{file: "URL"}]}) - /file\s*:\s*['"]([^'"]+\.m3u8[^'"]*)['"]/i, // Direct M3U8 link in a var or object e.g. file: "URL.m3u8" - /src\s*:\s*['"]([^'"]+\.m3u8[^'"]*)['"]/i, // Direct M3U8 link e.g. src: "URL.m3u8" - /loadSource\(['"]([^'"]+\.m3u8[^'"]*)['"]\)/i, // For .loadSource("URL.m3u8") - /new\s+Player\([^)]*\{\s*src\s*:\s*['"]([^'"]+)['"]\s*\}\s*\)/i // For new Player({src: "URL"}) - ]; - for (const regex of sourcesRegexes) { - const sourcesMatch = scriptContent.match(regex); - // For regexes that capture a JSON array-like structure in group 1 and then the URL in group 2 (first two regexes) - if (regex.source.includes('file') && regex.source.includes('sources')) { // Heuristic to identify complex sources regex capturing group 2 - if (sourcesMatch && sourcesMatch[2]) { // URL is in group 2 - console.log(`[VidSrc - SRCRCP] Found M3U8 URL (script complex): ${sourcesMatch[2]}`); - sourcesFound = [{ quality: 'default', url: sourcesMatch[2] }]; - return false; // break cheerio loop - } - } - // For simpler regexes where URL is in group 1 - else if (sourcesMatch && sourcesMatch[1]) { - console.log(`[VidSrc - SRCRCP] Found M3U8 URL (script simple): ${sourcesMatch[1]}`); - sourcesFound = [{ quality: 'default', url: sourcesMatch[1] }]; - return false; // break cheerio loop - } - } - // Fallback: Look for any absolute .m3u8 URL within the script tag - if (!sourcesFound) { - const m3u8GenericMatch = scriptContent.match(/['"](https?:\/\/[^'"\s]+\.m3u8[^'"\s]*)['"]/i); - if (m3u8GenericMatch && m3u8GenericMatch[1]) { - console.log(`[VidSrc - SRCRCP] Found M3U8 URL (script generic fallback): ${m3u8GenericMatch[1]}`); - sourcesFound = [{ quality: 'default', url: m3u8GenericMatch[1] }]; - return false; // break cheerio loop - } - } - } - }); - - if (sourcesFound && sourcesFound.length > 0) { - // Process the first valid M3U8 URL found, or return direct links - const m3u8Source = sourcesFound.find(s => s.url && s.url.includes('.m3u8')); - if (m3u8Source) { - console.log(`[VidSrc - SRCRCP] First M3U8 source from script: ${m3u8Source.url}`); - // Ensure URL is absolute - const absoluteM3u8Url = m3u8Source.url.startsWith('http') ? m3u8Source.url : new URL(m3u8Source.url, srcrcpUrl).href; - const m3u8FileFetch = await fetchWrapper(absoluteM3u8Url, { - headers: { "Referer": srcrcpUrl, "Accept": "*/*" }, - timeout: 10000 - }); - if (!m3u8FileFetch.ok) { - console.error(`[VidSrc - SRCRCP] Failed to fetch M3U8 from script source: ${absoluteM3u8Url}, status: ${m3u8FileFetch.status}`); - return null; - } - const m3u8Content = await m3u8FileFetch.text(); - return parseMasterM3U8(m3u8Content, absoluteM3u8Url); - } else { - // Assuming direct links if no .m3u8 found in the sources array - console.log(`[VidSrc - SRCRCP] Assuming direct links from script sources:`, sourcesFound); - return sourcesFound.map(s => ({ - quality: s.quality || s.label || 'auto', - url: s.url.startsWith('http') ? s.url : new URL(s.url, srcrcpUrl).href - })); - } - } - - console.warn(`[VidSrc - SRCRCP] No stream extraction method succeeded for ${srcrcpUrl}`); - return null; - } catch (error) { - console.error(`[VidSrc - SRCRCP] Error in SRCRCPhandler for ${srcrcpPath}:`, error); - return null; - } -} -async function rcpGrabber(html) { - const regex = /src:\s*'([^']*)'/; - const match = html.match(regex); - if (!match || !match[1]) - return null; - return { metadata: { image: "" }, data: match[1] }; -} -function getObject(id) { - const arr = id.split(':'); - return { id: arr[0], season: arr[1], episode: arr[2] }; -} -function getUrl(id, type) { - if (type === "movie") { - return `${SOURCE_URL}/movie/${id}`; - } - else { - const obj = getObject(id); - return `${SOURCE_URL}/tv/${obj.id}/${obj.season}-${obj.episode}`; - } -} -async function getStreamContent(id, type) { - const url = getUrl(id, type); - const embedRes = await fetchWrapper(url, { headers: { "Referer": SOURCE_URL } }); - if (!embedRes.ok) { - console.error(`Failed to fetch embed page ${url}: ${embedRes.status}`); - return []; - } - const embedResp = await embedRes.text(); - const { servers, title } = await serversLoad(embedResp); - const apiResponse = []; - - // MODIFIED: Process servers in parallel - const serverPromises = servers.map(async (server) => { - if (!server.dataHash) return null; // Skip servers without dataHash - - try { - const rcpUrl = `${BASEDOM}/rcp/${server.dataHash}`; - const rcpRes = await fetchWrapper(rcpUrl, { - headers: { 'Sec-Fetch-Dest': 'iframe', "Referer": url } - }); - if (!rcpRes.ok) { - console.warn(`RCP fetch failed for server ${server.name}: ${rcpRes.status}`); - return null; // Failed to fetch RCP - } - const rcpHtml = await rcpRes.text(); - const rcpData = await rcpGrabber(rcpHtml); - - if (!rcpData || !rcpData.data) { - console.warn(`Skipping server ${server.name} due to missing rcp data.`); - return null; // Missing RCP data - } - - let streamDetails = null; - if (rcpData.data.startsWith("/prorcp/")) { - streamDetails = await PRORCPhandler(rcpData.data.replace("/prorcp/", "")); - } else if (rcpData.data.startsWith("/srcrcp/")) { - if (server.name === "Superembed" || server.name === "2Embed") { - console.warn(`[VidSrc] Skipping SRCRCP for known problematic server: ${server.name}`); - return null; - } - streamDetails = await SRCRCPhandler(rcpData.data, rcpUrl); // Pass rcpUrl as referer - } else { - console.warn(`Unhandled rcp data type for server ${server.name}: ${rcpData.data.substring(0, 50)}`); - return null; // Unhandled type - } - - if (streamDetails && streamDetails.length > 0) { - return { - name: title, - image: rcpData.metadata.image, - mediaId: id, - streams: streamDetails, - referer: BASEDOM, - }; - } else { - console.warn(`No stream details from handler for server ${server.name} (${rcpData.data})`); - return null; // No stream details found by the handler - } - } catch (e) { - console.error(`Error processing server ${server.name} (${server.dataHash}):`, e); - return null; // Error during server processing - } - }); - - const results = await Promise.all(serverPromises); - // Filter out null results (failed servers or no streams) and add valid results to apiResponse - results.forEach(result => { - if (result) { - apiResponse.push(result); - } - }); - // END MODIFICATION - - return apiResponse; -} -// --- Main execution logic (conditional) --- -async function main() { - const args = process.argv.slice(2); - if (args.length < 2) { - console.error("Usage: node vidsrcextractor.js "); - console.error("Example (movie): node vidsrcextractor.js tt0111161 movie"); - console.error("Example (series): node vidsrcextractor.js tt0944947:1:1 series"); // Game of Thrones S1E1 - return 1; // error code - } - const id = args[0]; - const type = args[1]; - if (type !== "movie" && type !== "series") { - console.error("Invalid type. Must be 'movie' or 'series'."); - return 1; - } - if (type === "series" && id.split(':').length < 3) { - console.error("Invalid series ID format. Expected 'ttID:season:episode' (e.g., tt0944947:1:1)."); - return 1; - } - console.log(`Fetching streams for ID: ${id}, Type: ${type}`); - try { - const results = await getStreamContent(id, type); - if (results && results.length > 0) { - console.log("Extracted Data:"); - console.log(JSON.stringify(results, null, 2)); - } else { - console.log("No streams found or an error occurred."); - } - return 0; - } catch (error) { - console.error("Error during extraction process:", error); - return 1; - } -} - -// Export the function for use in other modules -module.exports = { getStreamContent }; - -// Run main only if the script is executed directly -if (require.main === module) { - main() - .then(code => { - if (code !== 0) { - console.error(`[vidsrcextractor] Finished with non-zero code ${code}`); - } - }) - .catch(error => { - console.error("Unhandled error in main execution:", error); - }); -} diff --git a/providers/vixsrc.js b/providers/vixsrc.js index d7435ca..b93b9f1 100644 --- a/providers/vixsrc.js +++ b/providers/vixsrc.js @@ -1,214 +1,180 @@ -/** - * Vixsrc streaming provider integration (standalone) - * React Native compatible variant - no external addon framework - */ +const axios = require('axios'); -// Constants -const TMDB_API_KEY = "68e094699525b18a70bab2f86b1fa706"; const BASE_URL = 'https://vixsrc.to'; -let vixsrcUrl; -let subtitleApiUrl; -// Helper function to make HTTP requests with default headers -function makeRequest(url, options = {}) { - const defaultHeaders = { - 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - 'Accept': 'application/json,*/*', - 'Accept-Language': 'en-US,en;q=0.5', - 'Accept-Encoding': 'gzip, deflate', - 'Connection': 'keep-alive', - ...options.headers - }; +const VIXSRC_HEADERS = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150 Safari/537.36', + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Accept-Language': 'en-US,en;q=0.9', + 'Referer': BASE_URL, + 'Origin': BASE_URL +}; - return fetch(url, { - method: options.method || 'GET', - headers: defaultHeaders, - ...options - }) - .then(response => { - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - return response; - }) - .catch(error => { - console.error(`[Vixsrc] Request failed for ${url}: ${error.message}`); - throw error; +// Step 1: GET /api/movie/{id} or /api/tv/{id}/{s}/{e} → { src: "/embed/..." } +async function fetchApi(url) { + try { + const response = await axios.get(url, { headers: VIXSRC_HEADERS, timeout: 10000 }); + if (response.status !== 200 || !response.data) return null; + return response.data; + } catch { + return null; + } +} + +// Step 2: GET BASE_URL + sublink.src → HTML embed page +async function fetchEmbedPage(suburl) { + try { + const response = await axios.get(BASE_URL + suburl, { + headers: { ...VIXSRC_HEADERS, Accept: 'text/html,application/xhtml+xml,*/*' }, + timeout: 10000, + responseType: 'text' }); + if (response.status !== 200) return null; + return response.data; + } catch { + return null; + } } -// Helper function to get TMDB info -function getTmdbInfo(tmdbId, mediaType) { - const url = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY}`; +// Step 3: Extract token, expires, playlist URL from embed HTML +function extractTokenData(html) { + const token = html.match(/token["']\s*:\s*["']([^"']+)/)?.[1]; + const expires = html.match(/expires["']\s*:\s*["']([^"']+)/)?.[1]; + const playlist = html.match(/url\s*:\s*["']([^"']+)/)?.[1]; - return makeRequest(url) - .then(response => response.json()) - .then(data => ({ data })); -} + if (!token || !expires || !playlist) return null; -// Helper function to parse M3U8 playlist -// Removed obsolete parseM3U8Playlist implementation - -// Removed unused helpers getQualityFromResolution and resolveUrl - -// Helper function to extract stream URL from Vixsrc page -function extractStreamFromPage(url, contentType, contentId, seasonNum, episodeNum) { - - if (contentType === 'movie') { - vixsrcUrl = `${BASE_URL}/movie/${contentId}`; - subtitleApiUrl = `https://sub.wyzie.ru/search?id=${contentId}`; - } else { - vixsrcUrl = `${BASE_URL}/tv/${contentId}/${seasonNum}/${episodeNum}`; - subtitleApiUrl = `https://sub.wyzie.ru/search?id=${contentId}&season=${seasonNum}&episode=${episodeNum}`; + // Reject expired tokens (with 60s grace period) + if (parseInt(expires, 10) * 1000 - 60_000 < Date.now()) { + console.log('[Vixsrc] Token is expired'); + return null; } - console.log(`[Vixsrc] Fetching: ${vixsrcUrl}`); + return { token, expires, playlist }; +} - return makeRequest(vixsrcUrl, { +// Step 4: Append token params to master URL +function buildMasterUrl(tokenData) { + const { token, expires, playlist } = tokenData; + const sep = playlist.includes('?') ? '&' : '?'; + return `${playlist}${sep}token=${token}&expires=${expires}&h=1`; +} + +// Step 5: Fetch the master HLS playlist +async function fetchPlaylist(masterUrl, pageApiUrl) { + try { + const response = await axios.get(masterUrl, { + headers: { ...VIXSRC_HEADERS, Referer: pageApiUrl }, + timeout: 10000, + responseType: 'text' + }); + if (response.status !== 200) return null; + return response.data; + } catch { + return null; + } +} + +// Step 6: Parse HLS manifest for quality variants, audio tracks, subtitles +function parsePlaylist(content, masterUrl, pageApiUrl) { + const sources = []; + const subtitles = []; + const audioTracks = []; + + const lines = content.split('\n'); + + // Audio tracks + for (const line of lines) { + if (!line.startsWith('#EXT-X-MEDIA:TYPE=AUDIO')) continue; + const language = line.match(/LANGUAGE="([^"]+)"/)?.[1] ?? 'unknown'; + const label = line.match(/NAME="([^"]+)"/)?.[1] ?? 'Audio'; + audioTracks.push({ language, label }); + } + + // Subtitles + for (const line of lines) { + if (!line.startsWith('#EXT-X-MEDIA:TYPE=SUBTITLES')) continue; + const url = line.match(/URI="([^"]+)"/)?.[1]; + if (!url) continue; + const label = line.match(/NAME="([^"]+)"/)?.[1] ?? 'unknown'; + subtitles.push({ url, label, format: 'vtt' }); + } + + // Quality variants — find the highest resolution + const variantRegex = /#EXT-X-STREAM-INF:[^\n]*RESOLUTION=\d+x(\d+)[^\n]*\n([^\n]+)/g; + let match; + let bestResolution = 0; + while ((match = variantRegex.exec(content)) !== null) { + const res = parseInt(match[1], 10); + if (res > bestResolution) bestResolution = res; + } + + if (bestResolution === 0) return { sources: [], subtitles: [] }; + + sources.push({ + name: `Vixsrc - ${bestResolution}p`, + title: `Vixsrc - ${bestResolution}p`, + url: masterUrl, + quality: `${bestResolution}p`, + provider: 'Vixsrc', headers: { - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' + 'Referer': pageApiUrl, + 'User-Agent': VIXSRC_HEADERS['User-Agent'] } - }) - .then(response => response.text()) - .then(html => { - console.log(`[Vixsrc] HTML length: ${html.length} characters`); + }); - let masterPlaylistUrl = null; - - // Method 1: Look for window.masterPlaylist (primary method) - if (html.includes('window.masterPlaylist')) { - console.log('[Vixsrc] Found window.masterPlaylist'); - - const urlMatch = html.match(/url:\s*['"]([^'"]+)['"]/); - const tokenMatch = html.match(/['"]?token['"]?\s*:\s*['"]([^'"]+)['"]/); - const expiresMatch = html.match(/['"]?expires['"]?\s*:\s*['"]([^'"]+)['"]/); - - if (urlMatch && tokenMatch && expiresMatch) { - const baseUrl = urlMatch[1]; - const token = tokenMatch[1]; - const expires = expiresMatch[1]; - - console.log('[Vixsrc] Extracted tokens:'); - console.log(` - Base URL: ${baseUrl}`); - console.log(` - Token: ${token.substring(0, 20)}...`); - console.log(` - Expires: ${expires}`); - - // Construct the master playlist URL - if (baseUrl.includes('?b=1')) { - masterPlaylistUrl = `${baseUrl}&token=${token}&expires=${expires}&h=1&lang=en`; - } else { - masterPlaylistUrl = `${baseUrl}?token=${token}&expires=${expires}&h=1&lang=en`; - } - - console.log(`[Vixsrc] Constructed master playlist URL: ${masterPlaylistUrl}`); - } - } - - // Method 2: Look for direct .m3u8 URLs - if (!masterPlaylistUrl) { - const m3u8Match = html.match(/(https?:\/\/[^'"\s]+\.m3u8[^'"\s]*)/); - if (m3u8Match) { - masterPlaylistUrl = m3u8Match[1]; - console.log('[Vixsrc] Found direct .m3u8 URL:', masterPlaylistUrl); - } - } - - // Method 3: Look for stream URLs in script tags - if (!masterPlaylistUrl) { - const scriptMatches = html.match(/]*>(.*?)<\/script>/gs); - if (scriptMatches) { - for (const script of scriptMatches) { - const streamMatch = script.match(/['"]?(https?:\/\/[^'"\s]+(?:\.m3u8|playlist)[^'"\s]*)/); - if (streamMatch) { - masterPlaylistUrl = streamMatch[1]; - console.log('[Vixsrc] Found stream in script:', masterPlaylistUrl); - break; - } - } - } - } - - if (!masterPlaylistUrl) { - console.log('[Vixsrc] No master playlist URL found'); - return null; - } - - return { masterPlaylistUrl, subtitleApiUrl }; - }); + return { sources, subtitles }; } -// Helper function to get subtitles -function getSubtitles(subtitleApiUrl) { - return makeRequest(subtitleApiUrl) - .then(response => response.json()) - .then(subtitleData => { - // Find English subtitle track (same logic as original) - let subtitleTrack = subtitleData.find(track => - track.display.includes('English') && (track.encoding === 'ASCII' || track.encoding === 'UTF-8') - ); - - if (!subtitleTrack) { - subtitleTrack = subtitleData.find(track => track.display.includes('English') && track.encoding === 'CP1252'); - } - - if (!subtitleTrack) { - subtitleTrack = subtitleData.find(track => track.display.includes('English') && track.encoding === 'CP1250'); - } - - if (!subtitleTrack) { - subtitleTrack = subtitleData.find(track => track.display.includes('English') && track.encoding === 'CP850'); - } - - const subtitles = subtitleTrack ? subtitleTrack.url : ''; - console.log(subtitles ? `[Vixsrc] Found subtitles: ${subtitles}` : '[Vixsrc] No English subtitles found'); - return subtitles; - }) - .catch(error => { - console.log('[Vixsrc] Subtitle fetch failed:', error.message); - return ''; - }); -} - -// Main function to get streams -function getVixsrcStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) { +async function getVixsrcStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) { console.log(`[Vixsrc] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`); - return getTmdbInfo(tmdbId, mediaType) - .then(() => extractStreamFromPage(null, mediaType, tmdbId, seasonNum, episodeNum)) - .then(streamData => { - if (!streamData) { - console.log('[Vixsrc] No stream data found'); - return []; - } + // Build API URL + let apiUrl; + if (mediaType === 'movie') { + apiUrl = `${BASE_URL}/api/movie/${tmdbId}`; + } else { + apiUrl = `${BASE_URL}/api/tv/${tmdbId}/${seasonNum}/${episodeNum}`; + } - const { masterPlaylistUrl, subtitleApiUrl } = streamData; + console.log(`[Vixsrc] Step 1 - Calling API: ${apiUrl}`); + const apiData = await fetchApi(apiUrl); + if (!apiData || !apiData.src) { + console.log('[Vixsrc] No src returned from API'); + return []; + } + console.log(`[Vixsrc] Step 2 - Fetching embed page: ${BASE_URL}${apiData.src}`); - // Return single master playlist with Auto quality - console.log('[Vixsrc] Returning master playlist with Auto quality...'); + const html = await fetchEmbedPage(apiData.src); + if (!html) { + console.log('[Vixsrc] Failed to fetch embed page'); + return []; + } + console.log(`[Vixsrc] Embed HTML length: ${html.length} characters`); - // Get subtitles - return getSubtitles(subtitleApiUrl) - .then(subtitles => { - // Return single stream with master playlist - const tmdbStreams = [{ - name: "Vixsrc", - title: "Auto Quality Stream", - url: masterPlaylistUrl, - quality: '1080p', - provider: 'Vidsrc', - headers: { - 'Referer': vixsrcUrl, - 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36' - } - }]; + const tokenData = extractTokenData(html); + if (!tokenData) { + console.log('[Vixsrc] Could not extract token/expires/playlist from embed HTML'); + return []; + } - console.log('[Vixsrc] Successfully processed 1 stream with Auto quality'); - return tmdbStreams; - }); - }) - .catch(error => { - console.error(`[Vixsrc] Error in getVixsrcStreams: ${error.message}`); - return []; - }); + const masterUrl = buildMasterUrl(tokenData); + console.log(`[Vixsrc] Step 3 - Master URL: ${masterUrl}`); + + const playlistContent = await fetchPlaylist(masterUrl, apiUrl); + if (!playlistContent) { + console.log('[Vixsrc] Failed to fetch HLS playlist'); + return []; + } + + const { sources, subtitles } = parsePlaylist(playlistContent, masterUrl, apiUrl); + + if (sources.length === 0) { + console.log('[Vixsrc] No streams found in HLS playlist'); + return []; + } + + console.log(`[Vixsrc] Successfully extracted ${sources.length} stream(s). Subtitles: ${subtitles.length}`); + return sources; } module.exports = { getVixsrcStreams }; diff --git a/public/config.html b/public/config.html index 7a28612..86315bf 100644 --- a/public/config.html +++ b/public/config.html @@ -29,15 +29,15 @@

-
- +
+
Made with ❤️ by Inside4ndroid
- +

Server Status

diff --git a/public/index.js b/public/index.js index 7e7d536..a0e0e5a 100644 --- a/public/index.js +++ b/public/index.js @@ -154,15 +154,13 @@ function fillForm(data){ const tmdbProvidersEnabled = [ merged.enableShowboxProvider !== false, merged.enable4khdhubProvider !== false, - merged.enableMoviesmodProvider !== false, - merged.enableMp4hydraProvider !== false + merged.enableMoviesmodProvider !== false ].some(Boolean); tmdbBanner.style.display = (tmdbMissing && tmdbProvidersEnabled)? 'block':'none'; } const advBools = [ ['enable4khdhubProvider','adv_enable4khdhub'], ['enableMoviesmodProvider','adv_enableMoviesmod'], - ['enableMp4hydraProvider','adv_enableMp4hydra'], ['enableVidzeeProvider','adv_enableVidzee'], ['enableVixsrcProvider','adv_enableVixsrc'], ['enableProxy','adv_enableProxy'], @@ -246,7 +244,6 @@ async function save(){ const boolMap = { adv_enable4khdhub:'enable4khdhubProvider', adv_enableMoviesmod:'enableMoviesmodProvider', - adv_enableMp4hydra:'enableMp4hydraProvider', adv_enableVidzee:'enableVidzeeProvider', adv_enableVixsrc:'enableVixsrcProvider', adv_enableProxy:'enableProxy', @@ -339,7 +336,6 @@ async function clearAll(){ enableShowboxProvider: true, enable4khdhubProvider: true, enableMoviesmodProvider: true, - enableMp4hydraProvider: true, enableVidzeeProvider: true, enableVixsrcProvider: true, enableProxy: false, @@ -449,7 +445,7 @@ async function loadServerStatus(){ } } async function runProviderFunctionalChecks(){ - const targetTmdb = '293660'; // TMDB ID provided + const targetTmdb = '550'; // TMDB ID provided const provEnhanced = document.getElementById('statusProvidersEnhanced'); const summary = document.getElementById('providerCheckSummary'); if(!provEnhanced) return; diff --git a/restart.trigger b/restart.trigger index 204b0a8..964ce21 100644 --- a/restart.trigger +++ b/restart.trigger @@ -1 +1 @@ -1758225404146 \ No newline at end of file +1778930133625 \ No newline at end of file diff --git a/utils/auth-users.json b/utils/auth-users.json index 588c488..3c43fb5 100644 --- a/utils/auth-users.json +++ b/utils/auth-users.json @@ -3,7 +3,7 @@ "users": [ { "username": "admin", - "hash": "7fffd5f131a733035eaf443cd99905c0:f1cfa876f9b84fede40cd618a4352221f7ab36e4f8bead432ba5d866b06cb9fcc2903ce623d8286bb541f8d28490fc692d0bf3fadda08002265374915cdb27b5" + "hash": "3c2efa431b6de0bee19847236e2c296b:d88ccc2ee950eda254a03d073bc55bbc8d0efc263c93c4e20307154dc90fadbc3141dcc9556a150b39b52088bb8ad06b120bc8832d7d8dc35918e710803d207c" } ] } \ No newline at end of file diff --git a/utils/config.js b/utils/config.js index 7ae74d0..11a786a 100644 --- a/utils/config.js +++ b/utils/config.js @@ -57,7 +57,7 @@ function getProviderNames() { const providersDir = path.join(process.cwd(), 'providers'); if (!fs.existsSync(providersDir)) return []; return fs.readdirSync(providersDir) - .filter(file => file.endsWith('.js') && file !== 'registry.js' && file !== 'vidsrcextractor.js') + .filter(file => file.endsWith('.js') && file !== 'registry.js') .map(file => path.parse(file).name.toLowerCase()); } catch (e) { console.warn('[config] Failed to scan providers directory:', e.message); diff --git a/utils/user-config.json b/utils/user-config.json index a9c3692..ab2985b 100644 --- a/utils/user-config.json +++ b/utils/user-config.json @@ -1,19 +1,28 @@ { "defaultProviders": [], "configVersion": 1, - "tmdbApiKeys": [], - "enableShowboxProvider": true, - "febboxCookies": [], - "enable4khdhubProvider": true, - "enableMoviesmodProvider": true, - "enableMp4hydraProvider": true, - "enableVidzeeProvider": true, - "enableVixsrcProvider": true, + "tmdbApiKeys": [ + "338a47b75eab45d9e64e67088f910f93" + ], + "enableShowboxProvider": false, + "febboxCookies": [ + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3Nzg5MjM2NDksIm5iZiI6MTc3ODkyMzY0OSwiZXhwIjoxODEwMDI3NjY5LCJkYXRhIjp7InVpZCI6ODc2NTEwLCJ0b2tlbiI6IjYxNmZlZDdjZDRjMzY1OWJmNWMzNTg5MGUzOTQ4Y2MyIn19.ESauZ9IInk5Iafpt-zLI64u4hMzsby_Spy0tkTJ6KuY" + ], + "enable4khdhubProvider": false, + "enableMoviesmodProvider": false, + "enableVidzeeProvider": false, + "enableVixsrcProvider": false, "enableProxy": false, "disableCache": false, "enablePStreamApi": true, "disableUrlValidation": false, "disable4khdhubUrlValidation": false, "excludeCodecsRaw": "{\"excludeDV\":false,\"excludeHDR\":false}", - "minQualitiesRaw": "all" + "minQualitiesRaw": "all", + "enableUhdmoviesProvider": false, + "enableVidlinkProvider": false, + "enableVideasyProvider": false, + "enableLordflixProvider": false, + "port": 8787, + "enableNotorrentProvider": false } \ No newline at end of file