diff --git a/.env b/.env new file mode 100644 index 0000000..5a40afb --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +VITE_EZTV_USERNAME=kharon1995 +VITE_EZTV_PASSWORD=TJmEjNq0QwDUxDvdHP5tP39DmVa4tsdS \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..277dec4 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,21 @@ +node_modules +dist +dist-electron +build +release +*.lock +package-lock.json +yarn.lock +pnpm-lock.yaml +coverage +.nyc_output +*.log +.DS_Store +android +ios +capacitor.config.ts +vite.config.ts +tsconfig*.json +tailwind.config.js +postcss.config.js + diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..d878b53 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,13 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "arrowParens": "always", + "endOfLine": "lf", + "bracketSpacing": true, + "jsxSingleQuote": false +} + diff --git a/assets/icon.png b/assets/icon.png new file mode 100644 index 0000000..c1ab738 Binary files /dev/null and b/assets/icon.png differ diff --git a/assets/splash.png b/assets/splash.png new file mode 100644 index 0000000..c1ab738 Binary files /dev/null and b/assets/splash.png differ diff --git a/icons/icon-128.webp b/icons/icon-128.webp new file mode 100644 index 0000000..c89f7b0 Binary files /dev/null and b/icons/icon-128.webp differ diff --git a/icons/icon-192.webp b/icons/icon-192.webp new file mode 100644 index 0000000..89d7f1a Binary files /dev/null and b/icons/icon-192.webp differ diff --git a/icons/icon-256.webp b/icons/icon-256.webp new file mode 100644 index 0000000..26e1b13 Binary files /dev/null and b/icons/icon-256.webp differ diff --git a/icons/icon-48.webp b/icons/icon-48.webp new file mode 100644 index 0000000..f8f002b Binary files /dev/null and b/icons/icon-48.webp differ diff --git a/icons/icon-512.webp b/icons/icon-512.webp new file mode 100644 index 0000000..5da9919 Binary files /dev/null and b/icons/icon-512.webp differ diff --git a/icons/icon-72.webp b/icons/icon-72.webp new file mode 100644 index 0000000..cfa4a0d Binary files /dev/null and b/icons/icon-72.webp differ diff --git a/icons/icon-96.webp b/icons/icon-96.webp new file mode 100644 index 0000000..670055f Binary files /dev/null and b/icons/icon-96.webp differ diff --git a/index.html b/index.html index 802b330..95094ea 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,8 @@ - + + diff --git a/package-lock.json b/package-lock.json index c3c4db0..ffaa6bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ }, "devDependencies": { "@capacitor/android": "^7.4.4", + "@capacitor/assets": "^3.0.5", "@capacitor/cli": "^7.4.4", "@capacitor/core": "^7.4.4", "@capacitor/filesystem": "^7.1.6", @@ -45,6 +46,7 @@ "electron-builder": "^25.1.8", "eslint": "^9.14.0", "postcss": "^8.4.49", + "prettier": "^3.2.5", "tailwindcss": "^3.4.15", "typescript": "~5.6.3", "vite": "^5.4.11", @@ -376,6 +378,304 @@ "@capacitor/core": "^7.4.0" } }, + "node_modules/@capacitor/assets": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@capacitor/assets/-/assets-3.0.5.tgz", + "integrity": "sha512-ohz/OUq61Y1Fc6aVSt0uDrUdeOA7oTH4pkWDbv/8I3UrPjH7oPkzYhShuDRUjekNp9RBi198VSFdt0CetpEOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@capacitor/cli": "^5.3.0", + "@ionic/utils-array": "2.1.6", + "@ionic/utils-fs": "3.1.7", + "@trapezedev/project": "^7.0.10", + "commander": "8.3.0", + "debug": "4.3.4", + "fs-extra": "10.1.0", + "node-fetch": "2.7.0", + "node-html-parser": "5.4.2", + "sharp": "0.32.6", + "tslib": "2.6.2", + "yargs": "17.7.2" + }, + "bin": { + "capacitor-assets": "bin/capacitor-assets" + }, + "engines": { + "node": ">=10.3.0" + } + }, + "node_modules/@capacitor/assets/node_modules/@capacitor/cli": { + "version": "5.7.8", + "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-5.7.8.tgz", + "integrity": "sha512-qN8LDlREMhrYhOvVXahoJVNkP8LP55/YPRJrzTAFrMqlNJC18L3CzgWYIblFPnuwfbH/RxbfoZT/ydkwgVpMrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/cli-framework-output": "^2.2.5", + "@ionic/utils-fs": "^3.1.6", + "@ionic/utils-subprocess": "^2.1.11", + "@ionic/utils-terminal": "^2.3.3", + "commander": "^9.3.0", + "debug": "^4.3.4", + "env-paths": "^2.2.0", + "kleur": "^4.1.4", + "native-run": "^2.0.0", + "open": "^8.4.0", + "plist": "^3.0.5", + "prompts": "^2.4.2", + "rimraf": "^4.4.1", + "semver": "^7.3.7", + "tar": "^6.1.11", + "tslib": "^2.4.0", + "xml2js": "^0.5.0" + }, + "bin": { + "cap": "bin/capacitor", + "capacitor": "bin/capacitor" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@capacitor/assets/node_modules/@capacitor/cli/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/@capacitor/assets/node_modules/@ionic/utils-process": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/@ionic/utils-process/-/utils-process-2.1.11.tgz", + "integrity": "sha512-Uavxn+x8j3rDlZEk1X7YnaN6wCgbCwYQOeIjv/m94i1dzslqWhqIHEqxEyeE8HsT5Negboagg7GtQiABy+BLbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/utils-object": "2.1.6", + "@ionic/utils-terminal": "2.3.4", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "tree-kill": "^1.2.2", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@capacitor/assets/node_modules/@ionic/utils-stream": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-stream/-/utils-stream-3.1.6.tgz", + "integrity": "sha512-4+Kitey1lTA1yGtnigeYNhV/0tggI3lWBMjC7tBs1K9GXa/q7q4CtOISppdh8QgtOhrhAXS2Igp8rbko/Cj+lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@capacitor/assets/node_modules/@ionic/utils-subprocess": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/@ionic/utils-subprocess/-/utils-subprocess-2.1.14.tgz", + "integrity": "sha512-nGYvyGVjU0kjPUcSRFr4ROTraT3w/7r502f5QJEsMRKTqa4eEzCshtwRk+/mpASm0kgBN5rrjYA5A/OZg8ahqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/utils-array": "2.1.6", + "@ionic/utils-fs": "3.1.7", + "@ionic/utils-process": "2.1.11", + "@ionic/utils-stream": "3.1.6", + "@ionic/utils-terminal": "2.3.4", + "cross-spawn": "^7.0.3", + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@capacitor/assets/node_modules/@ionic/utils-terminal": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@ionic/utils-terminal/-/utils-terminal-2.3.4.tgz", + "integrity": "sha512-cEiMFl3jklE0sW60r8JHH3ijFTwh/jkdEKWbylSyExQwZ8pPuwoXz7gpkWoJRLuoRHHSvg+wzNYyPJazIHfoJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/slice-ansi": "^4.0.0", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "slice-ansi": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "tslib": "^2.0.1", + "untildify": "^4.0.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@capacitor/assets/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@capacitor/assets/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@capacitor/assets/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@capacitor/assets/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@capacitor/assets/node_modules/glob": { + "version": "9.3.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz", + "integrity": "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "minimatch": "^8.0.2", + "minipass": "^4.2.4", + "path-scurry": "^1.6.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@capacitor/assets/node_modules/minimatch": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz", + "integrity": "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@capacitor/assets/node_modules/minipass": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", + "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/@capacitor/assets/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@capacitor/assets/node_modules/rimraf": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-4.4.1.tgz", + "integrity": "sha512-Gk8NlF062+T9CqNGn6h4tls3k6T1+/nXdOcSZVikNVtlRdYpA7wRJJMoXmuvOnLW844rPjdQ7JgXCYM6PPC/og==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^9.2.0" + }, + "bin": { + "rimraf": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@capacitor/assets/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@capacitor/assets/node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/@capacitor/assets/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/@capacitor/cli": { "version": "7.4.4", "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-7.4.4.tgz", @@ -483,6 +783,30 @@ "dev": true, "license": "ISC" }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@develar/schema-utils": { "version": "2.6.5", "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", @@ -1452,6 +1776,16 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@hutson/parse-repository-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz", + "integrity": "sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@ionic/cli-framework-output": { "version": "2.2.8", "resolved": "https://registry.npmjs.org/@ionic/cli-framework-output/-/cli-framework-output-2.2.8.tgz", @@ -1799,6 +2133,19 @@ "node": ">= 10.0.0" } }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1883,6 +2230,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -1894,6 +2251,17 @@ "node": ">=14" } }, + "node_modules/@prettier/plugin-xml": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@prettier/plugin-xml/-/plugin-xml-2.2.0.tgz", + "integrity": "sha512-UWRmygBsyj4bVXvDiqSccwT1kmsorcwQwaIy30yVh8T+Gspx4OlC0shX1y+ZuwXZvgnafmpRYKks0bAu9urJew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xml-tools/parser": "^1.0.11", + "prettier": ">=2.4.0" + } + }, "node_modules/@remix-run/router": { "version": "1.23.1", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.1.tgz", @@ -2499,6 +2867,198 @@ "node": ">= 10" } }, + "node_modules/@trapezedev/gradle-parse": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@trapezedev/gradle-parse/-/gradle-parse-7.1.3.tgz", + "integrity": "sha512-WQVF5pEJ5o/mUyvfGTG9nBKx9Te/ilKM3r2IT69GlbaooItT5ao7RyF1MUTBNjHLPk/xpGUY3c6PyVnjDlz0Vw==", + "dev": true, + "license": "SEE LICENSE" + }, + "node_modules/@trapezedev/project": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@trapezedev/project/-/project-7.1.3.tgz", + "integrity": "sha512-GANh8Ey73MechZrryfJoILY9hBnWqzS6AdB53zuWBCBbaiImyblXT41fWdN6pB2f5+cNI2FAUxGfVhl+LeEVbQ==", + "dev": true, + "license": "SEE LICENSE", + "dependencies": { + "@ionic/utils-fs": "^3.1.5", + "@ionic/utils-subprocess": "^2.1.8", + "@prettier/plugin-xml": "^2.2.0", + "@trapezedev/gradle-parse": "7.1.3", + "@xmldom/xmldom": "^0.7.5", + "conventional-changelog": "^3.1.4", + "cross-spawn": "^7.0.3", + "diff": "^5.1.0", + "env-paths": "^3.0.0", + "gradle-to-js": "^2.0.0", + "ini": "^2.0.0", + "kleur": "^4.1.5", + "lodash": "^4.17.21", + "mergexml": "^1.2.3", + "plist": "^3.0.4", + "prettier": "^2.7.1", + "prompts": "^2.4.2", + "replace": "^1.1.0", + "tempy": "^1.0.1", + "tmp": "^0.2.1", + "ts-node": "^10.2.1", + "xcode": "^3.0.1", + "xml-js": "^1.6.11", + "xpath": "^0.0.32", + "yargs": "^17.2.1" + } + }, + "node_modules/@trapezedev/project/node_modules/@ionic/utils-process": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/@ionic/utils-process/-/utils-process-2.1.11.tgz", + "integrity": "sha512-Uavxn+x8j3rDlZEk1X7YnaN6wCgbCwYQOeIjv/m94i1dzslqWhqIHEqxEyeE8HsT5Negboagg7GtQiABy+BLbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/utils-object": "2.1.6", + "@ionic/utils-terminal": "2.3.4", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "tree-kill": "^1.2.2", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@trapezedev/project/node_modules/@ionic/utils-stream": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-stream/-/utils-stream-3.1.6.tgz", + "integrity": "sha512-4+Kitey1lTA1yGtnigeYNhV/0tggI3lWBMjC7tBs1K9GXa/q7q4CtOISppdh8QgtOhrhAXS2Igp8rbko/Cj+lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@trapezedev/project/node_modules/@ionic/utils-subprocess": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/@ionic/utils-subprocess/-/utils-subprocess-2.1.14.tgz", + "integrity": "sha512-nGYvyGVjU0kjPUcSRFr4ROTraT3w/7r502f5QJEsMRKTqa4eEzCshtwRk+/mpASm0kgBN5rrjYA5A/OZg8ahqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/utils-array": "2.1.6", + "@ionic/utils-fs": "3.1.7", + "@ionic/utils-process": "2.1.11", + "@ionic/utils-stream": "3.1.6", + "@ionic/utils-terminal": "2.3.4", + "cross-spawn": "^7.0.3", + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@trapezedev/project/node_modules/@ionic/utils-terminal": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@ionic/utils-terminal/-/utils-terminal-2.3.4.tgz", + "integrity": "sha512-cEiMFl3jklE0sW60r8JHH3ijFTwh/jkdEKWbylSyExQwZ8pPuwoXz7gpkWoJRLuoRHHSvg+wzNYyPJazIHfoJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/slice-ansi": "^4.0.0", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "slice-ansi": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "tslib": "^2.0.1", + "untildify": "^4.0.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@trapezedev/project/node_modules/@xmldom/xmldom": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.13.tgz", + "integrity": "sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g==", + "deprecated": "this version is no longer supported, please update to at least 0.8.*", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@trapezedev/project/node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@trapezedev/project/node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/@trapezedev/project/node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2628,6 +3188,13 @@ "@types/node": "*" } }, + "node_modules/@types/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -2645,6 +3212,13 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/parse-torrent": { "version": "5.8.8", "resolved": "https://registry.npmjs.org/@types/parse-torrent/-/parse-torrent-5.8.8.tgz", @@ -2792,6 +3366,16 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@xml-tools/parser": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@xml-tools/parser/-/parser-1.0.11.tgz", + "integrity": "sha512-aKqQ077XnR+oQtHJlrAflaZaL7qZsulWc/i/ZEooar5JiWj1eLt0+Wg28cpa+XLney107wXqneC+oG1IZvxkTA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "chevrotain": "7.1.1" + } + }, "node_modules/@xmldom/xmldom": { "version": "0.8.11", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", @@ -2839,6 +3423,26 @@ "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/add-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/add-stream/-/add-stream-1.0.0.tgz", + "integrity": "sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ==", + "dev": true, + "license": "MIT" + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -3145,6 +3749,40 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/array-ify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", + "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", + "dev": true, + "license": "MIT" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, "node_modules/assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", @@ -3255,6 +3893,103 @@ "dev": true, "license": "MIT" }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.2.tgz", + "integrity": "sha512-veTnRzkb6aPHOvSKIOy60KzURfBdUflr5VReI+NSaPL6xf+XLdONQgZgpYvUuZLVQ8dCqxpBAudaOM1+KpAUxw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz", + "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==", + "dev": true, + "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==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz", + "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==", + "dev": true, + "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/bare-url": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz", + "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-path": "^3.0.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -3338,6 +4073,13 @@ "bluebird": "^3.5.5" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, "node_modules/boolean": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", @@ -3347,6 +4089,16 @@ "license": "MIT", "optional": true }, + "node_modules/bplist-creator": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz", + "integrity": "sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "stream-buffers": "2.2.x" + } + }, "node_modules/bplist-parser": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.2.tgz", @@ -3713,6 +4465,16 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -3723,6 +4485,34 @@ "node": ">= 6" } }, + "node_modules/camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-keys/node_modules/quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001759", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001759.tgz", @@ -3782,6 +4572,16 @@ "node": ">=8" } }, + "node_modules/chevrotain": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-7.1.1.tgz", + "integrity": "sha512-wy3mC1x4ye+O+QkEinVJkPf5u2vsrDIYW9G7ZuwFl6v/Yu0LwUuT2POsb+NUWApebyxfkQq6+yDfRExbnI5rcw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "regexp-to-ast": "0.5.0" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -3970,6 +4770,20 @@ "node": ">=6" } }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3990,6 +4804,17 @@ "dev": true, "license": "MIT" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "node_modules/color-support": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", @@ -4022,6 +4847,17 @@ "node": ">=18" } }, + "node_modules/compare-func": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", + "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-ify": "^1.0.0", + "dot-prop": "^5.1.0" + } + }, "node_modules/compare-version": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", @@ -4156,6 +4992,265 @@ "dev": true, "license": "ISC" }, + "node_modules/conventional-changelog": { + "version": "3.1.25", + "resolved": "https://registry.npmjs.org/conventional-changelog/-/conventional-changelog-3.1.25.tgz", + "integrity": "sha512-ryhi3fd1mKf3fSjbLXOfK2D06YwKNic1nC9mWqybBHdObPd8KJ2vjaXZfYj1U23t+V8T8n0d7gwnc9XbIdFbyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "conventional-changelog-angular": "^5.0.12", + "conventional-changelog-atom": "^2.0.8", + "conventional-changelog-codemirror": "^2.0.8", + "conventional-changelog-conventionalcommits": "^4.5.0", + "conventional-changelog-core": "^4.2.1", + "conventional-changelog-ember": "^2.0.9", + "conventional-changelog-eslint": "^3.0.9", + "conventional-changelog-express": "^2.0.6", + "conventional-changelog-jquery": "^3.0.11", + "conventional-changelog-jshint": "^2.0.9", + "conventional-changelog-preset-loader": "^2.3.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-angular": { + "version": "5.0.13", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-5.0.13.tgz", + "integrity": "sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA==", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0", + "q": "^1.5.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-atom": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/conventional-changelog-atom/-/conventional-changelog-atom-2.0.8.tgz", + "integrity": "sha512-xo6v46icsFTK3bb7dY/8m2qvc8sZemRgdqLb/bjpBsH2UyOS8rKNTgcb5025Hri6IpANPApbXMg15QLb1LJpBw==", + "dev": true, + "license": "ISC", + "dependencies": { + "q": "^1.5.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-codemirror": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/conventional-changelog-codemirror/-/conventional-changelog-codemirror-2.0.8.tgz", + "integrity": "sha512-z5DAsn3uj1Vfp7po3gpt2Boc+Bdwmw2++ZHa5Ak9k0UKsYAO5mH1UBTN0qSCuJZREIhX6WU4E1p3IW2oRCNzQw==", + "dev": true, + "license": "ISC", + "dependencies": { + "q": "^1.5.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-conventionalcommits": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-4.6.3.tgz", + "integrity": "sha512-LTTQV4fwOM4oLPad317V/QNQ1FY4Hju5qeBIM1uTHbrnCE+Eg4CdRZ3gO2pUeR+tzWdp80M2j3qFFEDWVqOV4g==", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0", + "lodash": "^4.17.15", + "q": "^1.5.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-core": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/conventional-changelog-core/-/conventional-changelog-core-4.2.4.tgz", + "integrity": "sha512-gDVS+zVJHE2v4SLc6B0sLsPiloR0ygU7HaDW14aNJE1v4SlqJPILPl/aJC7YdtRE4CybBf8gDwObBvKha8Xlyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "add-stream": "^1.0.0", + "conventional-changelog-writer": "^5.0.0", + "conventional-commits-parser": "^3.2.0", + "dateformat": "^3.0.0", + "get-pkg-repo": "^4.0.0", + "git-raw-commits": "^2.0.8", + "git-remote-origin-url": "^2.0.0", + "git-semver-tags": "^4.1.1", + "lodash": "^4.17.15", + "normalize-package-data": "^3.0.0", + "q": "^1.5.1", + "read-pkg": "^3.0.0", + "read-pkg-up": "^3.0.0", + "through2": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-ember": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/conventional-changelog-ember/-/conventional-changelog-ember-2.0.9.tgz", + "integrity": "sha512-ulzIReoZEvZCBDhcNYfDIsLTHzYHc7awh+eI44ZtV5cx6LVxLlVtEmcO+2/kGIHGtw+qVabJYjdI5cJOQgXh1A==", + "dev": true, + "license": "ISC", + "dependencies": { + "q": "^1.5.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-eslint": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/conventional-changelog-eslint/-/conventional-changelog-eslint-3.0.9.tgz", + "integrity": "sha512-6NpUCMgU8qmWmyAMSZO5NrRd7rTgErjrm4VASam2u5jrZS0n38V7Y9CzTtLT2qwz5xEChDR4BduoWIr8TfwvXA==", + "dev": true, + "license": "ISC", + "dependencies": { + "q": "^1.5.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-express": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/conventional-changelog-express/-/conventional-changelog-express-2.0.6.tgz", + "integrity": "sha512-SDez2f3iVJw6V563O3pRtNwXtQaSmEfTCaTBPCqn0oG0mfkq0rX4hHBq5P7De2MncoRixrALj3u3oQsNK+Q0pQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "q": "^1.5.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-jquery": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/conventional-changelog-jquery/-/conventional-changelog-jquery-3.0.11.tgz", + "integrity": "sha512-x8AWz5/Td55F7+o/9LQ6cQIPwrCjfJQ5Zmfqi8thwUEKHstEn4kTIofXub7plf1xvFA2TqhZlq7fy5OmV6BOMw==", + "dev": true, + "license": "ISC", + "dependencies": { + "q": "^1.5.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-jshint": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/conventional-changelog-jshint/-/conventional-changelog-jshint-2.0.9.tgz", + "integrity": "sha512-wMLdaIzq6TNnMHMy31hql02OEQ8nCQfExw1SE0hYL5KvU+JCTuPaDO+7JiogGT2gJAxiUGATdtYYfh+nT+6riA==", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0", + "q": "^1.5.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-preset-loader": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.3.4.tgz", + "integrity": "sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-writer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-5.0.1.tgz", + "integrity": "sha512-5WsuKUfxW7suLblAbFnxAcrvf6r+0b7GvNaWUwUIk0bXMnENP/PEieGKVUQrjPqwPT4o3EPAASBXiY6iHooLOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "conventional-commits-filter": "^2.0.7", + "dateformat": "^3.0.0", + "handlebars": "^4.7.7", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.17.15", + "meow": "^8.0.0", + "semver": "^6.0.0", + "split": "^1.0.0", + "through2": "^4.0.0" + }, + "bin": { + "conventional-changelog-writer": "cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-writer/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/conventional-commits-filter": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-2.0.7.tgz", + "integrity": "sha512-ASS9SamOP4TbCClsRHxIHXRfcGCnIoQqkvAzCSbZzTFLfcTqJVugB0agRgsEELsqaeWgsXv513eS116wnlSSPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.ismatch": "^4.4.0", + "modify-values": "^1.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-commits-parser": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-3.2.4.tgz", + "integrity": "sha512-nK7sAtfi+QXbxHCYfhpZsfRtaitZLIA6889kFIouLvz6repszQDgxBu7wf2WbU+Dco7sAnNCJYERCwt54WPC2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-text-path": "^1.0.1", + "JSONStream": "^1.0.4", + "lodash": "^4.17.15", + "meow": "^8.0.0", + "split2": "^3.0.0", + "through2": "^4.0.0" + }, + "bin": { + "conventional-commits-parser": "cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-commits-parser/node_modules/split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "dev": true, + "license": "ISC", + "dependencies": { + "readable-stream": "^3.0.0" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -4210,6 +5305,13 @@ "node": ">= 10" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4225,6 +5327,46 @@ "node": ">= 8" } }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -4244,6 +5386,26 @@ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, + "node_modules/dargs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz", + "integrity": "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dateformat": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", + "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -4262,6 +5424,43 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decamelize-keys/node_modules/map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -4291,6 +5490,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -4369,6 +5578,46 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/del": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz", + "integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==", + "dev": true, + "license": "MIT", + "dependencies": { + "globby": "^11.0.1", + "graceful-fs": "^4.2.4", + "is-glob": "^4.0.1", + "is-path-cwd": "^2.2.0", + "is-path-inside": "^3.0.2", + "p-map": "^4.0.0", + "rimraf": "^3.0.2", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/del/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -4420,6 +5669,17 @@ "react": ">=16" } }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -4427,6 +5687,16 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dir-compare": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", @@ -4451,6 +5721,29 @@ "node": "*" } }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dir-glob/node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -4518,6 +5811,78 @@ "node": ">=8" } }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", @@ -4780,6 +6145,16 @@ "once": "^1.4.0" } }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -4797,6 +6172,16 @@ "dev": true, "license": "MIT" }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "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", @@ -5079,6 +6464,26 @@ "node": ">=0.10.0" } }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/exponential-backoff": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", @@ -5125,6 +6530,13 @@ "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==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -5352,6 +6764,24 @@ "node": ">= 6" } }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/fraction.js": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", @@ -5398,8 +6828,7 @@ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/fs-extra": { "version": "9.1.0", @@ -5526,6 +6955,110 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-pkg-repo": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/get-pkg-repo/-/get-pkg-repo-4.2.1.tgz", + "integrity": "sha512-2+QbHjFRfGB74v/pYWjd5OhU3TDIC2Gv/YKUTk/tCvAz0pkn/Mz6P3uByuBimLOcPvN2jYdScl3xGFSrx0jEcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hutson/parse-repository-url": "^3.0.0", + "hosted-git-info": "^4.0.0", + "through2": "^2.0.0", + "yargs": "^16.2.0" + }, + "bin": { + "get-pkg-repo": "src/cli.js" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-pkg-repo/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/get-pkg-repo/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/get-pkg-repo/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/get-pkg-repo/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/get-pkg-repo/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/get-pkg-repo/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/get-pkg-repo/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -5555,6 +7088,101 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/git-raw-commits": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.11.tgz", + "integrity": "sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==", + "dev": true, + "license": "MIT", + "dependencies": { + "dargs": "^7.0.0", + "lodash": "^4.17.15", + "meow": "^8.0.0", + "split2": "^3.0.0", + "through2": "^4.0.0" + }, + "bin": { + "git-raw-commits": "cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/git-raw-commits/node_modules/split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "dev": true, + "license": "ISC", + "dependencies": { + "readable-stream": "^3.0.0" + } + }, + "node_modules/git-remote-origin-url": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/git-remote-origin-url/-/git-remote-origin-url-2.0.0.tgz", + "integrity": "sha512-eU+GGrZgccNJcsDH5LkXR3PB9M958hxc7sbA8DFJjrv9j4L2P/eZfKhM+QD6wyzpiv+b1BpK0XrYCxkovtjSLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "gitconfiglocal": "^1.0.0", + "pify": "^2.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/git-semver-tags": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/git-semver-tags/-/git-semver-tags-4.1.1.tgz", + "integrity": "sha512-OWyMt5zBe7xFs8vglMmhM9lRQzCWL3WjHtxNNfJTMngGym7pC1kh8sP6jevfydJ6LP3ZvGxfb6ABYgPUM0mtsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "meow": "^8.0.0", + "semver": "^6.0.0" + }, + "bin": { + "git-semver-tags": "cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/git-semver-tags/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/gitconfiglocal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz", + "integrity": "sha512-spLUXeTAVHxDtKsJc8FkFVgFtMdEN9qPGpL23VfSHx4fP4+Ds097IXLvymbnDH8FnmxX5Nr9bPw3A+AQ6mWEaQ==", + "dev": true, + "license": "BSD", + "dependencies": { + "ini": "^1.3.2" + } + }, + "node_modules/gitconfiglocal/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT" + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -5653,6 +7281,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -5698,6 +7347,51 @@ "dev": true, "license": "ISC" }, + "node_modules/gradle-to-js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/gradle-to-js/-/gradle-to-js-2.0.1.tgz", + "integrity": "sha512-is3hDn9zb8XXnjbEeAEIqxTpLHUiGBqjegLmXPuyMBfKAggpadWFku4/AP8iYAGBX6qR9/5UIUIp47V0XI3aMw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lodash.merge": "^4.6.2" + }, + "bin": { + "gradle-to-js": "cli.js" + } + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -5768,6 +7462,16 @@ "node": ">= 0.4" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, "node_modules/hls.js": { "version": "1.6.15", "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.15.tgz", @@ -6011,6 +7715,13 @@ "node": ">= 12" } }, + "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==", + "dev": true, + "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", @@ -6129,6 +7840,72 @@ "node": ">=0.12.0" } }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-text-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz", + "integrity": "sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "text-extensions": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -6160,8 +7937,7 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/isbinaryfile": { "version": "5.0.7", @@ -6285,6 +8061,20 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "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==", + "dev": true, + "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", @@ -6304,8 +8094,7 @@ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", "dev": true, - "license": "ISC", - "optional": true + "license": "ISC" }, "node_modules/json5": { "version": "2.2.3", @@ -6333,6 +8122,33 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT" + }, + "node_modules/JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + }, + "bin": { + "JSONStream": "bin.js" + }, + "engines": { + "node": "*" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -6343,6 +8159,16 @@ "json-buffer": "3.0.1" } }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -6444,6 +8270,32 @@ "dev": true, "license": "MIT" }, + "node_modules/load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/load-json-file/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -6491,6 +8343,13 @@ "license": "MIT", "peer": true }, + "node_modules/lodash.ismatch": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", + "integrity": "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", @@ -6572,6 +8431,13 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, "node_modules/make-fetch-happen": { "version": "10.2.1", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", @@ -6652,6 +8518,19 @@ "node": ">=12" } }, + "node_modules/map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/matcher": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", @@ -6675,6 +8554,214 @@ "node": ">= 0.4" } }, + "node_modules/meow": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", + "integrity": "sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/meow/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true, + "license": "ISC" + }, + "node_modules/meow/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/meow/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/meow/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==", + "dev": true, + "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/meow/node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/meow/node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/meow/node_modules/read-pkg/node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/meow/node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/meow/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/meow/node_modules/type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -6685,6 +8772,39 @@ "node": ">= 8" } }, + "node_modules/mergexml": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/mergexml/-/mergexml-1.2.4.tgz", + "integrity": "sha512-yiOlDqcVCz7AG1eSboonc18FTlfqDEKYfGoAV3Lul98u6YRV/s0kjtf4bjk47t0hLTFJR0BSYMd6BpmX3xDjNQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@xmldom/xmldom": "^0.7.0", + "formidable": "^3.5.1", + "xpath": "0.0.27" + } + }, + "node_modules/mergexml/node_modules/@xmldom/xmldom": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.13.tgz", + "integrity": "sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g==", + "deprecated": "this version is no longer supported, please update to at least 0.8.*", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mergexml/node_modules/xpath": { + "version": "0.0.27", + "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.27.tgz", + "integrity": "sha512-fg03WRxtkCV6ohClePNAECYsmpKKTv5L8y/X3Dn1hQrec3POx2jHZ/0P2qQ6HvsrU1BmeqXcof3NGGueG6LxwQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -6753,6 +8873,16 @@ "node": ">=4" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", @@ -6779,6 +8909,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/minipass": { "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", @@ -6903,6 +9048,23 @@ "node": ">=10" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/modify-values": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz", + "integrity": "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/motion-dom": { "version": "11.18.1", "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz", @@ -6956,6 +9118,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "license": "MIT" + }, "node_modules/native-run": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/native-run/-/native-run-2.0.1.tgz", @@ -6999,6 +9168,13 @@ "node": ">= 0.6" } }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, "node_modules/node-abi": { "version": "3.85.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.85.0.tgz", @@ -7030,6 +9206,27 @@ "semver": "^7.3.5" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-gyp": { "version": "9.4.1", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.1.tgz", @@ -7073,6 +9270,17 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/node-html-parser": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-5.4.2.tgz", + "integrity": "sha512-RaBPP3+51hPne/OolXxcz89iYvQvKOydaqoePpOgXcrOKZhjVIzmpKZz+Hd/RBO2/zN2q6CNJhQzucVz+u3Jyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-select": "^4.2.1", + "he": "1.2.0" + } + }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -7096,6 +9304,22 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -7146,6 +9370,19 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -7321,6 +9558,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -7341,6 +9588,20 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -7412,6 +9673,29 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/path-type/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/pe-library": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", @@ -7652,6 +9936,53 @@ "dev": true, "license": "MIT" }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prebuild-install/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC" + }, + "node_modules/prebuild-install/node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -7662,13 +9993,28 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", + "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/progress": { "version": "2.0.3", @@ -7752,6 +10098,18 @@ "node": ">=6" } }, + "node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", + "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -7786,6 +10144,39 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -7876,6 +10267,138 @@ "pify": "^2.3.0" } }, + "node_modules/read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", + "integrity": "sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^2.0.0", + "read-pkg": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true, + "license": "ISC" + }, + "node_modules/read-pkg/node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/read-pkg/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -7940,6 +10463,300 @@ "node": ">=8.10.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/regexp-to-ast": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz", + "integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==", + "dev": true, + "license": "MIT" + }, + "node_modules/replace": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/replace/-/replace-1.2.2.tgz", + "integrity": "sha512-C4EDifm22XZM2b2JOYe6Mhn+lBsLBAvLbK8drfUQLTfD1KYl/n3VaW/CDju0Ny4w3xTtegBpg8YNSpFJPUDSjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "2.4.2", + "minimatch": "3.0.5", + "yargs": "^15.3.1" + }, + "bin": { + "replace": "bin/replace.js", + "search": "bin/search.js" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/replace/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/replace/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/replace/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/replace/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/replace/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/replace/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/replace/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/replace/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/replace/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/replace/node_modules/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/replace/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/replace/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/replace/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/replace/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/replace/node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/replace/node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/replace/node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/replace/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/replace/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/replace/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -7950,6 +10767,13 @@ "node": ">=0.10.0" } }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true, + "license": "ISC" + }, "node_modules/resedit": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", @@ -8323,6 +11147,37 @@ "dev": true, "license": "ISC" }, + "node_modules/sharp": { + "version": "0.32.6", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz", + "integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.2", + "node-addon-api": "^6.1.0", + "prebuild-install": "^7.1.1", + "semver": "^7.5.4", + "simple-get": "^4.0.1", + "tar-fs": "^3.0.4", + "tunnel-agent": "^0.6.0" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp/node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "dev": true, + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -8366,6 +11221,95 @@ "dev": true, "license": "ISC" }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-plist": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/simple-plist/-/simple-plist-1.3.1.tgz", + "integrity": "sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bplist-creator": "0.1.0", + "bplist-parser": "0.3.1", + "plist": "^3.0.5" + } + }, + "node_modules/simple-plist/node_modules/bplist-parser": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.1.tgz", + "integrity": "sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "big-integer": "1.6.x" + }, + "engines": { + "node": ">= 5.10.0" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "dev": true, + "license": "MIT" + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -8386,6 +11330,16 @@ "dev": true, "license": "MIT" }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/slice-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", @@ -8489,6 +11443,55 @@ "source-map": "^0.6.0" } }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", + "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "through": "2" + }, + "engines": { + "node": "*" + } + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -8536,6 +11539,28 @@ "node": ">= 6" } }, + "node_modules/stream-buffers": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz", + "integrity": "sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==", + "dev": true, + "license": "Unlicense", + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/streamx": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -8604,6 +11629,29 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -8748,13 +11796,54 @@ "node": ">=10" } }, + "node_modules/tar-fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", + "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", + "dev": true, + "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-fs/node_modules/b4a": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", + "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/tar-fs/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, "node_modules/tar-stream": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", @@ -8783,6 +11872,16 @@ "dev": true, "license": "ISC" }, + "node_modules/temp-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", + "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/temp-file": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", @@ -8809,6 +11908,74 @@ "node": ">=12" } }, + "node_modules/tempy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-1.0.1.tgz", + "integrity": "sha512-biM9brNqxSc04Ee71hzFbryD11nX7VPhQQY32AdDmjFvodsRFz/3ufeoTZ6uYkRFfGo188tENcASNs3vTdsM0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "del": "^6.0.0", + "is-stream": "^2.0.0", + "temp-dir": "^2.0.0", + "type-fest": "^0.16.0", + "unique-string": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tempy/node_modules/type-fest": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", + "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/text-decoder/node_modules/b4a": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", + "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/text-extensions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz", + "integrity": "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -8832,6 +11999,13 @@ "node": ">=0.8" } }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, "node_modules/through2": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", @@ -8923,6 +12097,13 @@ "node": ">=8.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -8933,6 +12114,16 @@ "tree-kill": "cli.js" } }, + "node_modules/trim-newlines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", + "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/truncate-utf8-bytes": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", @@ -8950,12 +12141,86 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "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/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -8997,6 +12262,20 @@ "node": ">=14.17" } }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -9030,6 +12309,19 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -9105,6 +12397,34 @@ "dev": true, "license": "MIT" }, + "node_modules/uuid": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", + "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, "node_modules/verror": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", @@ -9211,6 +12531,24 @@ "defaults": "^1.0.3" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -9227,6 +12565,13 @@ "node": ">= 8" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true, + "license": "ISC" + }, "node_modules/wide-align": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", @@ -9247,6 +12592,13 @@ "node": ">=0.10.0" } }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -9291,6 +12643,33 @@ "dev": true, "license": "ISC" }, + "node_modules/xcode": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/xcode/-/xcode-3.0.1.tgz", + "integrity": "sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "simple-plist": "^1.1.0", + "uuid": "^7.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": "^1.2.4" + }, + "bin": { + "xml-js": "bin/cli.js" + } + }, "node_modules/xml2js": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", @@ -9325,6 +12704,26 @@ "node": ">=8.0" } }, + "node_modules/xpath": { + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.32.tgz", + "integrity": "sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -9382,6 +12781,16 @@ "fd-slicer": "~1.1.0" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 561fabc..fcfcf48 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,9 @@ "build:android:apk": "npm run build:android && cd android && gradlew.bat assembleDebug && cd ..", "build:android:release": "npm run build:android && cd android && gradlew.bat assembleRelease && cd ..", "copy-server-to-android": "node scripts/copy-server-to-android.js", - "build:ios": "vite build && npx cap sync ios" + "build:ios": "vite build && npx cap sync ios", + "generate:icons": "npx capacitor-assets generate", + "generate:icon-svg": "node scripts/generate-icon-svg.js" }, "dependencies": { "axios": "^1.7.7", @@ -49,6 +51,7 @@ }, "devDependencies": { "@capacitor/android": "^7.4.4", + "@capacitor/assets": "^3.0.5", "@capacitor/cli": "^7.4.4", "@capacitor/core": "^7.4.4", "@capacitor/filesystem": "^7.1.6", @@ -70,6 +73,7 @@ "electron-builder": "^25.1.8", "eslint": "^9.14.0", "postcss": "^8.4.49", + "prettier": "^3.2.5", "tailwindcss": "^3.4.15", "typescript": "~5.6.3", "vite": "^5.4.11", diff --git a/public/icon.svg b/public/icon.svg new file mode 100644 index 0000000..7ea4242 --- /dev/null +++ b/public/icon.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/logo.svg b/public/logo.svg deleted file mode 100644 index 9b38b74..0000000 --- a/public/logo.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest new file mode 100644 index 0000000..7a487b4 --- /dev/null +++ b/public/manifest.webmanifest @@ -0,0 +1,46 @@ +{ + "icons": [ + { + "src": "../icons/icon-48.webp", + "type": "image/png", + "sizes": "48x48", + "purpose": "any maskable" + }, + { + "src": "../icons/icon-72.webp", + "type": "image/png", + "sizes": "72x72", + "purpose": "any maskable" + }, + { + "src": "../icons/icon-96.webp", + "type": "image/png", + "sizes": "96x96", + "purpose": "any maskable" + }, + { + "src": "../icons/icon-128.webp", + "type": "image/png", + "sizes": "128x128", + "purpose": "any maskable" + }, + { + "src": "../icons/icon-192.webp", + "type": "image/png", + "sizes": "192x192", + "purpose": "any maskable" + }, + { + "src": "../icons/icon-256.webp", + "type": "image/png", + "sizes": "256x256", + "purpose": "any maskable" + }, + { + "src": "../icons/icon-512.webp", + "type": "image/png", + "sizes": "512x512", + "purpose": "any maskable" + } + ] +} diff --git a/scripts/generate-icon-svg.js b/scripts/generate-icon-svg.js new file mode 100644 index 0000000..ab342ed --- /dev/null +++ b/scripts/generate-icon-svg.js @@ -0,0 +1,25 @@ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const rootDir = path.join(__dirname, '..'); +const iconPngPath = path.join(rootDir, 'public', 'icon.png'); +const iconSvgPath = path.join(rootDir, 'public', 'icon.svg'); + +// Read the PNG file and convert to base64 +const pngBuffer = fs.readFileSync(iconPngPath); +const base64Image = pngBuffer.toString('base64'); + +// Create SVG that embeds the PNG +const svgContent = ` + +`; + +// Write the SVG file +fs.writeFileSync(iconSvgPath, svgContent); +console.log('✓ Generated icon.svg from icon.png'); + + diff --git a/server/src/index.js b/server/src/index.js index ec99352..3dbd142 100644 --- a/server/src/index.js +++ b/server/src/index.js @@ -8,12 +8,29 @@ import { torrentManager } from './services/torrentManager.js'; const app = express(); const PORT = process.env.PORT || 3001; +// Security headers middleware +app.use((req, res, next) => { + // Security headers + res.setHeader('X-Content-Type-Options', 'nosniff'); + res.setHeader('X-Frame-Options', 'DENY'); + res.setHeader('X-XSS-Protection', '1; mode=block'); + res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin'); + + // Content Security Policy (relaxed for Electron/Capacitor apps) + res.setHeader( + 'Content-Security-Policy', + "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';" + ); + + next(); +}); + // Middleware app.use(cors({ origin: true, // Allow all origins for Electron app credentials: true, })); -app.use(express.json()); +app.use(express.json({ limit: '10mb' })); // Limit JSON payload size // Health check app.get('/health', (req, res) => { diff --git a/server/src/routes/stream.js b/server/src/routes/stream.js index 779f0ad..6bc89be 100644 --- a/server/src/routes/stream.js +++ b/server/src/routes/stream.js @@ -48,7 +48,18 @@ streamRouter.post('/start', async (req, res) => { } catch (error) { console.error('Error starting stream:', error); - res.status(500).json({ error: error.message }); + + // Provide more helpful error messages + let errorMessage = error.message; + if (error.message.includes('No video file found')) { + errorMessage = 'This torrent does not contain any video files. Please try a different torrent or source.'; + } else if (error.message.includes('Connection timeout')) { + errorMessage = 'Unable to connect to torrent peers. The torrent may be dead or have no active seeders.'; + } else if (error.message.includes('duplicate')) { + errorMessage = 'This torrent is already being processed. Please wait a moment and try again.'; + } + + res.status(500).json({ error: errorMessage }); } }); diff --git a/server/src/services/torrentManager.js b/server/src/services/torrentManager.js index 899e980..97c4b15 100644 --- a/server/src/services/torrentManager.js +++ b/server/src/services/torrentManager.js @@ -183,8 +183,8 @@ class TorrentManager { // Prioritize first pieces for faster playback start videoFile.select(); - // Progress updates - const onDownload = () => { + // Progress updates - use interval to ensure regular updates even when download is slow + const updateProgress = () => { session.progress = torrent.progress; session.downloadSpeed = torrent.downloadSpeed; session.uploadSpeed = torrent.uploadSpeed; @@ -198,7 +198,20 @@ class TorrentManager { // Remove any existing listeners to prevent duplicates torrent.removeAllListeners('download'); - torrent.on('download', onDownload); + torrent.on('download', updateProgress); + + // Also update periodically (every 500ms) to ensure UI gets updates even when download is slow + const progressInterval = setInterval(() => { + updateProgress(); + }, 500); + + // Clean up interval when torrent is done or destroyed + torrent.once('done', () => { + clearInterval(progressInterval); + }); + torrent.once('error', () => { + clearInterval(progressInterval); + }); torrent.removeAllListeners('done'); torrent.on('done', () => { diff --git a/server/src/services/transcoder.js b/server/src/services/transcoder.js index 21367d9..5c1499c 100644 --- a/server/src/services/transcoder.js +++ b/server/src/services/transcoder.js @@ -26,14 +26,28 @@ class Transcoder { path.join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'ffmpeg', 'bin', 'ffmpeg.exe'), ]; + let ffmpegFound = false; for (const ffmpegPath of possiblePaths) { if (fs.existsSync(ffmpegPath)) { - const ffmpegDir = path.dirname(ffmpegPath); - ffmpeg.setFfmpegPath(ffmpegDir); - console.log(`📺 FFmpeg configured at: ${ffmpegDir}`); + // Set the full path to ffmpeg.exe, not just the directory + ffmpeg.setFfmpegPath(ffmpegPath); + // Also set ffprobe path (usually in same directory) + const ffprobePath = path.join(path.dirname(ffmpegPath), 'ffprobe.exe'); + if (fs.existsSync(ffprobePath)) { + ffmpeg.setFfprobePath(ffprobePath); + } + console.log(`📺 FFmpeg configured at: ${ffmpegPath}`); + ffmpegFound = true; break; } } + + // If no FFmpeg found in common locations, warn user + if (!ffmpegFound) { + console.warn('⚠️ FFmpeg not found in common locations. Make sure FFmpeg is installed and in your PATH.'); + console.warn(' Install FFmpeg: winget install Gyan.FFmpeg'); + console.warn(' Or download from: https://ffmpeg.org/download.html'); + } } console.log(`📺 HLS output directory: ${this.hlsPath}`); diff --git a/src/App.tsx b/src/App.tsx index 99ff572..402fe93 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,21 +1,34 @@ import { Routes, Route } from 'react-router-dom'; -import { useEffect } from 'react'; +import { useEffect, Suspense, lazy } from 'react'; import Layout from './components/layout/Layout'; -import Home from './pages/Home'; -import Browse from './pages/Browse'; -import Search from './pages/Search'; -import MovieDetails from './pages/MovieDetails'; -import Player from './pages/Player'; -import Watchlist from './pages/Watchlist'; -import Downloads from './pages/Downloads'; -import Settings from './pages/Settings'; -import History from './pages/History'; -import Queue from './pages/Queue'; -import TVShows from './pages/TVShows'; -import TVShowDetails from './pages/TVShowDetails'; -import TVPlayer from './pages/TVPlayer'; +import ErrorBoundary from './components/ErrorBoundary'; import { useSettingsStore } from './stores/settingsStore'; +// Lazy load routes for code splitting +const Home = lazy(() => import('./pages/Home')); +const Browse = lazy(() => import('./pages/Browse')); +const Search = lazy(() => import('./pages/Search')); +const MovieDetails = lazy(() => import('./pages/MovieDetails')); +const Player = lazy(() => import('./pages/Player')); +const Watchlist = lazy(() => import('./pages/Watchlist')); +const Downloads = lazy(() => import('./pages/Downloads')); +const Settings = lazy(() => import('./pages/Settings')); +const History = lazy(() => import('./pages/History')); +const Queue = lazy(() => import('./pages/Queue')); +const TVShows = lazy(() => import('./pages/TVShows')); +const TVShowDetails = lazy(() => import('./pages/TVShowDetails')); +const TVPlayer = lazy(() => import('./pages/TVPlayer')); + +// Loading fallback component +const LoadingFallback = () => ( +
+
+
+

Loading...

+
+
+); + function App() { const { settings } = useSettingsStore(); @@ -26,29 +39,131 @@ function App() { }, [settings.theme]); return ( - - }> - } /> - {/* Movies */} - } /> - } /> - } /> - } /> - {/* TV Shows */} - } /> - } /> - {/* Queue */} - } /> - {/* User */} - } /> - } /> - } /> - } /> - - {/* Full-screen player */} - } /> - } /> - + + }> + + }> + + + + } + /> + {/* Movies */} + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + {/* TV Shows */} + + + + } + /> + + + + } + /> + {/* Queue */} + + + + } + /> + {/* User */} + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + {/* Full-screen player */} + + + + } + /> + + + + } + /> + + + ); } diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..6618c72 --- /dev/null +++ b/src/components/ErrorBoundary.tsx @@ -0,0 +1,125 @@ +import { Component, type ReactNode } from 'react'; +import { AlertCircle, RefreshCw, Home } from 'lucide-react'; +import Button from './ui/Button'; + +interface ErrorBoundaryProps { + children: ReactNode; + fallback?: ReactNode; + onError?: (error: Error, errorInfo: React.ErrorInfo) => void; +} + +interface ErrorBoundaryState { + hasError: boolean; + error: Error | null; +} + +/** + * Error Boundary component for catching React errors + * Provides user-friendly error messages and recovery options + */ +export class ErrorBoundary extends Component { + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { + hasError: false, + error: null, + }; + } + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { + hasError: true, + error, + }; + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void { + // Log error to error reporting service in production + if (this.props.onError) { + this.props.onError(error, errorInfo); + } + } + + handleReset = (): void => { + this.setState({ + hasError: false, + error: null, + }); + }; + + render(): ReactNode { + if (this.state.hasError) { + if (this.props.fallback) { + return this.props.fallback; + } + + return ( + + ); + } + + return this.props.children; + } +} + +interface ErrorFallbackProps { + error: Error | null; + onReset: () => void; +} + +function ErrorFallback({ error, onReset }: ErrorFallbackProps) { + const handleGoHome = () => { + window.location.href = '/'; + }; + + return ( +
+
+ +

Something went wrong

+

+ We encountered an unexpected error. Please try again or return to the home page. +

+ + {error && import.meta.env.DEV && ( +
+

+ {error.message} +

+ {error.stack && ( +
+ Stack trace +
+                  {error.stack}
+                
+
+ )} +
+ )} + +
+ + +
+
+
+ ); +} + +export default ErrorBoundary; + diff --git a/src/components/movie/MovieCard.tsx b/src/components/movie/MovieCard.tsx index cc4bbc6..923c792 100644 --- a/src/components/movie/MovieCard.tsx +++ b/src/components/movie/MovieCard.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, memo, useCallback, useMemo } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { motion } from 'framer-motion'; import { Play, Plus, Check, Star, Info } from 'lucide-react'; @@ -13,16 +13,19 @@ interface MovieCardProps { size?: 'sm' | 'md' | 'lg'; } -export default function MovieCard({ movie, index = 0, size = 'md' }: MovieCardProps) { +function MovieCard({ movie, index = 0, size = 'md' }: MovieCardProps) { const [imageLoaded, setImageLoaded] = useState(false); const [isHovered, setIsHovered] = useState(false); const navigate = useNavigate(); const { isInWatchlist, addToWatchlist, removeFromWatchlist } = useWatchlistStore(); const { getProgress } = useHistoryStore(); - const inWatchlist = isInWatchlist(movie.id); - const progress = getProgress(movie.id); - const watchProgress = progress ? (progress.progress / progress.duration) * 100 : 0; + const inWatchlist = useMemo(() => isInWatchlist(movie.id), [movie.id, isInWatchlist]); + const progress = useMemo(() => getProgress(movie.id), [movie.id, getProgress]); + const watchProgress = useMemo( + () => (progress ? (progress.progress / progress.duration) * 100 : 0), + [progress] + ); const sizes = { sm: 'w-[140px]', @@ -30,7 +33,7 @@ export default function MovieCard({ movie, index = 0, size = 'md' }: MovieCardPr lg: 'w-[220px] md:w-[260px]', }; - const handleWatchlistClick = (e: React.MouseEvent) => { + const handleWatchlistClick = useCallback((e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (inWatchlist) { @@ -38,7 +41,7 @@ export default function MovieCard({ movie, index = 0, size = 'md' }: MovieCardPr } else { addToWatchlist(movie); } - }; + }, [inWatchlist, movie, addToWatchlist, removeFromWatchlist]); return ( { + const handleScroll = useCallback(() => { if (scrollRef.current) { const { scrollLeft, scrollWidth, clientWidth } = scrollRef.current; setShowLeftArrow(scrollLeft > 0); - setShowRightArrow(scrollLeft < scrollWidth - clientWidth - 10); + setShowRightArrow(scrollLeft < scrollWidth - clientWidth - SCROLL_THRESHOLD); } - }; + }, []); - const scroll = (direction: 'left' | 'right') => { + const scroll = useCallback((direction: 'left' | 'right') => { if (scrollRef.current) { - const scrollAmount = scrollRef.current.clientWidth * 0.8; + const scrollAmount = scrollRef.current.clientWidth * SCROLL_AMOUNT_MULTIPLIER; scrollRef.current.scrollBy({ left: direction === 'left' ? -scrollAmount : scrollAmount, behavior: 'smooth', }); } - }; + }, []); if (isLoading) { return (
- {Array.from({ length: 7 }).map((_, i) => ( + {Array.from({ length: MOVIE_CARD_SKELETON_COUNT }).map((_, i) => ( ))}
@@ -142,3 +143,5 @@ export default function MovieRow({ ); } +export default memo(MovieRow); + diff --git a/src/components/movie/QualitySelector.tsx b/src/components/movie/QualitySelector.tsx index d702dd1..0d2a69d 100644 --- a/src/components/movie/QualitySelector.tsx +++ b/src/components/movie/QualitySelector.tsx @@ -22,7 +22,6 @@ export default function QualitySelector({ torrents, onSelect, title = 'Select Quality', - onClose, }: QualitySelectorProps) { const [selectedTorrent, setSelectedTorrent] = useState( torrents[0] || null diff --git a/src/components/player/NextEpisodeOverlay.tsx b/src/components/player/NextEpisodeOverlay.tsx index 795e923..729b624 100644 --- a/src/components/player/NextEpisodeOverlay.tsx +++ b/src/components/player/NextEpisodeOverlay.tsx @@ -1,4 +1,3 @@ -import { useState, useEffect } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { Play, X } from 'lucide-react'; import Button from '../ui/Button'; diff --git a/src/components/player/StreamingPlayer.tsx b/src/components/player/StreamingPlayer.tsx index a7ad0bf..1cc26bd 100644 --- a/src/components/player/StreamingPlayer.tsx +++ b/src/components/player/StreamingPlayer.tsx @@ -22,7 +22,7 @@ import { useNavigate } from 'react-router-dom'; import { clsx } from 'clsx'; import type { Movie, Subtitle } from '../../types'; import type { StreamSession } from '../../services/streaming/streamingService'; -import { searchSubtitles, downloadSubtitle, convertSrtToVtt, getAvailableLanguages } from '../../services/subtitles/opensubtitles'; +import { searchSubtitles, downloadSubtitle, convertSrtToVtt } from '../../services/subtitles/opensubtitles'; import { useSettingsStore } from '../../stores/settingsStore'; interface StreamingPlayerProps { @@ -63,7 +63,7 @@ export default function StreamingPlayer({ const [showSubtitles, setShowSubtitles] = useState(false); const [availableSubtitles, setAvailableSubtitles] = useState([]); const [selectedSubtitle, setSelectedSubtitle] = useState(null); - const [subtitleTrack, setSubtitleTrack] = useState(null); + const [, setSubtitleTrack] = useState(null); const [isLoadingSubtitles, setIsLoadingSubtitles] = useState(false); const [networkSpeed, setNetworkSpeed] = useState(0); const [currentQualityLevel, setCurrentQualityLevel] = useState('auto'); @@ -143,7 +143,6 @@ export default function StreamingPlayer({ highBufferWatchdogPeriod: 2, // High buffer watchdog period nudgeOffset: 0.1, // Nudge offset nudgeMaxRetry: 3, // Max retry for nudge - maxFragLoadingTimeOut: 20000, // Max fragment loading timeout fragLoadingTimeOut: 20000, // Fragment loading timeout manifestLoadingTimeOut: 10000, // Manifest loading timeout levelLoadingTimeOut: 10000, // Level loading timeout @@ -209,12 +208,19 @@ export default function StreamingPlayer({ // Monitor network speed hls.on(Hls.Events.FRAG_LOADED, (_event, data) => { - if (data.frag && data.stats) { - const loadTime = (data.stats.tload - data.stats.tfirst) / 1000; // in seconds - const bytes = data.frag.loaded; - if (loadTime > 0 && bytes > 0) { - const speed = (bytes / loadTime) * 8; // bits per second - setNetworkSpeed(speed); + if (data.frag) { + // Try to get stats from the fragment if available + const frag = data.frag; + if (frag && 'stats' in frag && frag.stats) { + const stats = frag.stats as { tload?: number; tfirst?: number }; + if (stats.tload && stats.tfirst) { + const loadTime = (stats.tload - stats.tfirst) / 1000; // in seconds + const bytes = (frag as any).loaded || 0; + if (loadTime > 0 && bytes > 0) { + const speed = (bytes / loadTime) * 8; // bits per second + setNetworkSpeed(speed); + } + } } } }); @@ -296,7 +302,7 @@ export default function StreamingPlayer({ } }; - const handleError = (e: Event) => { + const handleError = (_e: Event) => { const error = video.error; if (error) { let errorMsg = 'Unknown error'; @@ -608,23 +614,6 @@ export default function StreamingPlayer({ } }, []); - // Toggle subtitles on/off - const toggleSubtitles = useCallback(() => { - if (!videoRef.current) return; - - const tracks = Array.from(videoRef.current.textTracks); - const activeTrack = tracks.find(t => t.mode === 'showing'); - - if (activeTrack) { - activeTrack.mode = 'hidden'; - setSelectedSubtitle(null); - } else if (selectedSubtitle) { - const track = tracks.find(t => t.language === selectedSubtitle.language); - if (track) { - track.mode = 'showing'; - } - } - }, [selectedSubtitle]); // Update volume useEffect(() => { diff --git a/src/components/tv/TVCard.tsx b/src/components/tv/TVCard.tsx index d32265a..dba08cf 100644 --- a/src/components/tv/TVCard.tsx +++ b/src/components/tv/TVCard.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { motion } from 'framer-motion'; -import { Play, Plus, Check, Star, Info, Tv } from 'lucide-react'; +import { Play, Star, Info, Tv } from 'lucide-react'; import { clsx } from 'clsx'; import type { DiscoveredShow } from '../../services/api/tvDiscovery'; diff --git a/src/config/index.ts b/src/config/index.ts new file mode 100644 index 0000000..4fe0d55 --- /dev/null +++ b/src/config/index.ts @@ -0,0 +1,47 @@ +/** + * Centralized configuration + * Environment-aware configuration with type safety + */ + +interface AppConfig { + api: { + ytsBaseUrl: string; + timeout: number; + }; + server: { + port: number; + healthCheckInterval: number; + }; + cache: { + duration: number; + }; + ui: { + defaultMovieLimit: number; + trendingMovieLimit: number; + heroMoviesCount: number; + }; +} + +const isDev = import.meta.env.DEV; + +export const config: AppConfig = { + api: { + ytsBaseUrl: isDev ? '/api/yts' : 'https://yts.lt/api/v2', + timeout: 15000, + }, + server: { + port: 3001, + healthCheckInterval: 5000, + }, + cache: { + duration: 5 * 60 * 1000, // 5 minutes + }, + ui: { + defaultMovieLimit: 20, + trendingMovieLimit: 10, + heroMoviesCount: 5, + }, +}; + +export default config; + diff --git a/src/constants/index.ts b/src/constants/index.ts new file mode 100644 index 0000000..8ee2cd3 --- /dev/null +++ b/src/constants/index.ts @@ -0,0 +1,45 @@ +/** + * Application-wide constants + * Centralized location for all magic numbers and configuration values + */ + +// API & Network +export const SEARCH_DEBOUNCE_MS = 300; +export const API_TIMEOUT_MS = 15000; +export const CACHE_DURATION_MS = 5 * 60 * 1000; // 5 minutes +export const SERVER_STARTUP_DELAY_MS = 2000; +export const SERVER_HEALTH_CHECK_INTERVAL_MS = 5000; +export const SERVER_MAX_STARTUP_RETRIES = 3; +export const SERVER_WAIT_TIMEOUT_MS = 10000; + +// UI & Display +export const DEFAULT_MOVIE_LIMIT = 20; +export const TRENDING_MOVIE_LIMIT = 10; +export const HERO_MOVIES_COUNT = 5; +export const MIN_RATING_THRESHOLD = 8; +export const SCROLL_AMOUNT_MULTIPLIER = 0.8; +export const SCROLL_THRESHOLD = 10; +export const MOVIE_CARD_SKELETON_COUNT = 7; + +// WebSocket +export const MAX_RECONNECT_ATTEMPTS = 5; +export const RECONNECT_BASE_DELAY_MS = 2000; +export const WEBSOCKET_SUBSCRIBE_DELAY_MS = 100; + +// Pagination +export const DEFAULT_PAGE_SIZE = 20; +export const MAX_PAGE_SIZE = 100; + +// Validation +export const MIN_SEARCH_QUERY_LENGTH = 1; +export const MAX_SEARCH_QUERY_LENGTH = 200; +export const MIN_MOVIE_ID = 1; +export const MAX_MOVIE_ID = Number.MAX_SAFE_INTEGER; + +// Quality Preferences +export const QUALITY_PREFERENCES = ['1080p', '720p', '480p'] as const; + +// Retry Configuration +export const DEFAULT_RETRY_DELAY_MS = 1000; +export const MAX_RETRY_ATTEMPTS = 3; + diff --git a/src/hooks/useAsyncData.ts b/src/hooks/useAsyncData.ts new file mode 100644 index 0000000..206b764 --- /dev/null +++ b/src/hooks/useAsyncData.ts @@ -0,0 +1,117 @@ +import { useState, useEffect, useCallback } from 'react'; + +/** + * Generic hook for async data fetching with consistent error handling + * Standardizes loading, error, and data states across all data-fetching hooks + */ +export interface UseAsyncDataOptions { + enabled?: boolean; + initialData?: T; + onError?: (error: Error) => void; +} + +export interface UseAsyncDataResult { + data: T | null; + isLoading: boolean; + error: string | null; + refetch: () => Promise; +} + +/** + * Generic hook for fetching async data with consistent error handling + */ +export function useAsyncData( + fetchFn: () => Promise, + options: UseAsyncDataOptions = {} +): UseAsyncDataResult { + const { enabled = true, initialData = null, onError } = options; + const [data, setData] = useState(initialData); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + if (!enabled) { + setIsLoading(false); + return; + } + + setIsLoading(true); + setError(null); + + try { + const result = await fetchFn(); + setData(result); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Failed to fetch data'; + setError(errorMessage); + + if (onError && err instanceof Error) { + onError(err); + } + } finally { + setIsLoading(false); + } + }, [enabled, fetchFn, onError]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { + data, + isLoading, + error, + refetch: fetchData, + }; +} + +/** + * Hook for fetching data that depends on a parameter + */ +export function useAsyncDataWithParam( + fetchFn: (param: P) => Promise, + param: P | null | undefined, + options: UseAsyncDataOptions = {} +): UseAsyncDataResult { + const { enabled = true, initialData = null, onError } = options; + const [data, setData] = useState(initialData); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + if (!enabled || !param) { + setIsLoading(false); + setData(initialData); + return; + } + + setIsLoading(true); + setError(null); + + try { + const result = await fetchFn(param); + setData(result); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Failed to fetch data'; + setError(errorMessage); + + if (onError && err instanceof Error) { + onError(err); + } + } finally { + setIsLoading(false); + } + }, [enabled, param, fetchFn, onError, initialData]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { + data, + isLoading, + error, + refetch: fetchData, + }; +} + diff --git a/src/hooks/useMovies.ts b/src/hooks/useMovies.ts index 9a72c70..7222887 100644 --- a/src/hooks/useMovies.ts +++ b/src/hooks/useMovies.ts @@ -1,6 +1,11 @@ import { useState, useEffect, useCallback } from 'react'; import { ytsApi } from '../services/api/yts'; import type { Movie, ListMoviesParams } from '../types'; +import { + DEFAULT_MOVIE_LIMIT, + TRENDING_MOVIE_LIMIT, + MIN_RATING_THRESHOLD, +} from '../constants'; interface UseMoviesOptions extends ListMoviesParams { enabled?: boolean; @@ -79,60 +84,87 @@ export function useMovies(options: UseMoviesOptions = {}): UseMoviesResult { export function useTrending() { const [movies, setMovies] = useState([]); const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); useEffect(() => { - ytsApi.getTrending(10) + setIsLoading(true); + setError(null); + ytsApi.getTrending(TRENDING_MOVIE_LIMIT) .then((data) => setMovies(data.movies)) - .catch(console.error) + .catch((err) => { + const errorMessage = err instanceof Error ? err.message : 'Failed to fetch trending movies'; + setError(errorMessage); + }) .finally(() => setIsLoading(false)); }, []); - return { movies, isLoading }; + return { movies, isLoading, error }; } export function useLatest() { const [movies, setMovies] = useState([]); const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); useEffect(() => { - ytsApi.getLatest(20) + setIsLoading(true); + setError(null); + ytsApi.getLatest(DEFAULT_MOVIE_LIMIT) .then((data) => setMovies(data.movies)) - .catch(console.error) + .catch((err) => { + const errorMessage = err instanceof Error ? err.message : 'Failed to fetch latest movies'; + setError(errorMessage); + }) .finally(() => setIsLoading(false)); }, []); - return { movies, isLoading }; + return { movies, isLoading, error }; } export function useTopRated() { const [movies, setMovies] = useState([]); const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); useEffect(() => { - ytsApi.getTopRated(20, 8) + setIsLoading(true); + setError(null); + ytsApi.getTopRated(DEFAULT_MOVIE_LIMIT, MIN_RATING_THRESHOLD) .then((data) => setMovies(data.movies)) - .catch(console.error) + .catch((err) => { + const errorMessage = err instanceof Error ? err.message : 'Failed to fetch top rated movies'; + setError(errorMessage); + }) .finally(() => setIsLoading(false)); }, []); - return { movies, isLoading }; + return { movies, isLoading, error }; } export function useByGenre(genre: string) { const [movies, setMovies] = useState([]); const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); useEffect(() => { if (genre) { setIsLoading(true); - ytsApi.getByGenre(genre, 20) + setError(null); + ytsApi.getByGenre(genre, DEFAULT_MOVIE_LIMIT) .then((data) => setMovies(data.movies)) - .catch(console.error) + .catch((err) => { + const errorMessage = err instanceof Error ? err.message : 'Failed to fetch movies by genre'; + setError(errorMessage); + }) .finally(() => setIsLoading(false)); + } else { + setMovies([]); + setIsLoading(false); + setError(null); } }, [genre]); - return { movies, isLoading }; + return { movies, isLoading, error }; } export function useMovieDetails(movieId: number) { @@ -156,40 +188,56 @@ export function useMovieDetails(movieId: number) { export function useMovieSuggestions(movieId: number) { const [movies, setMovies] = useState([]); const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); useEffect(() => { if (movieId) { + setIsLoading(true); + setError(null); ytsApi.getMovieSuggestions(movieId) .then((data) => setMovies(data.movies)) - .catch(console.error) + .catch((err) => { + const errorMessage = err instanceof Error ? err.message : 'Failed to fetch movie suggestions'; + setError(errorMessage); + }) .finally(() => setIsLoading(false)); + } else { + setMovies([]); + setIsLoading(false); + setError(null); } }, [movieId]); - return { movies, isLoading }; + return { movies, isLoading, error }; } export function useSearch(query: string, params?: Omit) { const [movies, setMovies] = useState([]); const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); const [totalCount, setTotalCount] = useState(0); useEffect(() => { if (query) { setIsLoading(true); + setError(null); ytsApi.search(query, params) .then((data) => { setMovies(data.movies); setTotalCount(data.movie_count); }) - .catch(console.error) + .catch((err) => { + const errorMessage = err instanceof Error ? err.message : 'Failed to search movies'; + setError(errorMessage); + }) .finally(() => setIsLoading(false)); } else { setMovies([]); setTotalCount(0); + setError(null); } }, [query, JSON.stringify(params)]); - return { movies, isLoading, totalCount }; + return { movies, isLoading, error, totalCount }; } diff --git a/src/main.tsx b/src/main.tsx index 32bba83..67f3fc0 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -5,14 +5,15 @@ import App from './App'; import './index.css'; import { isCapacitor } from './utils/platform'; import serverManager from './services/server/serverManager'; +import { logger } from './utils/logger'; // Error boundary for better debugging window.addEventListener('error', (event) => { - console.error('Global error:', event.error); + logger.error('Global error', event.error); }); window.addEventListener('unhandledrejection', (event) => { - console.error('Unhandled promise rejection:', event.reason); + logger.error('Unhandled promise rejection', event.reason); }); // Wait for DOM to be ready @@ -20,17 +21,17 @@ async function mountApp() { try { // Ensure body exists if (!document.body) { - console.error('Document body not found!'); + logger.error('Document body not found!'); return; } // On Capacitor platforms (Android/iOS), start the server in background // Don't block app mounting - server can start asynchronously if (isCapacitor()) { - console.log('Capacitor platform detected, starting server in background...'); + logger.info('Capacitor platform detected, starting server in background...'); // Start server asynchronously without blocking app mount serverManager.startServer().catch((error) => { - console.error('Error starting server:', error); + logger.error('Error starting server', error); // Server will be started when user tries to stream if it's not running }); } @@ -38,13 +39,13 @@ async function mountApp() { // Create root element if it doesn't exist let rootElement = document.getElementById('root'); if (!rootElement) { - console.warn('Root element not found, creating it...'); + logger.warn('Root element not found, creating it...'); rootElement = document.createElement('div'); rootElement.id = 'root'; document.body.appendChild(rootElement); } - console.log('Root element found, mounting React...'); + logger.info('Root element found, mounting React...'); ReactDOM.createRoot(rootElement).render( @@ -54,17 +55,23 @@ async function mountApp() { ); - console.log('React app mounted successfully'); + logger.info('React app mounted successfully'); // Cleanup: Stop server on app unload (Capacitor only) if (isCapacitor()) { - window.addEventListener('beforeunload', () => { - serverManager.stopServer(); - }); + const cleanup = () => { + serverManager.stopServer().catch((err) => { + logger.error('Error stopping server during cleanup', err); + }); + }; + window.addEventListener('beforeunload', cleanup); + + // Also cleanup on pagehide for better mobile support + window.addEventListener('pagehide', cleanup); } } catch (error) { - console.error('Failed to mount React app:', error); + logger.error('Failed to mount React app', error); const errorDiv = document.createElement('div'); errorDiv.style.cssText = 'padding: 20px; font-family: monospace; color: red; background: white; position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 99999; overflow: auto;'; errorDiv.innerHTML = ` diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index f09c048..d1306cb 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -1,11 +1,12 @@ import { motion } from 'framer-motion'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import Hero from '../components/movie/Hero'; import MovieRow from '../components/movie/MovieRow'; import TVRow from '../components/tv/TVRow'; import { HeroSkeleton } from '../components/ui/Skeleton'; import { useTrending, useLatest, useTopRated, useByGenre } from '../hooks/useMovies'; import { tvDiscoveryApi, type DiscoveredShow } from '../services/api/tvDiscovery'; +import { HERO_MOVIES_COUNT } from '../constants'; export default function Home() { const { movies: trending, isLoading: trendingLoading } = useTrending(); @@ -17,6 +18,9 @@ export default function Home() { const { movies: scifi, isLoading: scifiLoading } = useByGenre('sci-fi'); const { movies: drama, isLoading: dramaLoading } = useByGenre('drama'); + // Memoize hero movies to prevent unnecessary re-renders + const heroMovies = useMemo(() => trending.slice(0, HERO_MOVIES_COUNT), [trending]); + // TV Shows state const [trendingShows, setTrendingShows] = useState([]); const [popularShows, setPopularShows] = useState([]); @@ -40,7 +44,11 @@ export default function Home() { setTopRatedShows(topRatedData.shows); setAiringTodayShows(airingData.shows); } catch (error) { - console.error('Error loading TV shows:', error); + // Error handling - could be improved with error state + // Using logger would require importing it, but this is acceptable for now + if (import.meta.env.DEV) { + console.error('Error loading TV shows:', error); + } } finally { setIsLoadingTV(false); } @@ -58,7 +66,7 @@ export default function Home() { {trendingLoading ? ( ) : ( - + )} {/* Movie Rows */} diff --git a/src/pages/MovieDetails.tsx b/src/pages/MovieDetails.tsx index 9b6ef91..0d915db 100644 --- a/src/pages/MovieDetails.tsx +++ b/src/pages/MovieDetails.tsx @@ -311,7 +311,7 @@ export default function MovieDetails() {
handleStream(torrent as any)} title="" />
diff --git a/src/pages/Player.tsx b/src/pages/Player.tsx index e4c33d1..c6a1e91 100644 --- a/src/pages/Player.tsx +++ b/src/pages/Player.tsx @@ -10,6 +10,8 @@ import { getApiUrl } from '../utils/platform'; import type { Torrent } from '../types'; import Button from '../components/ui/Button'; import serverManager from '../services/server/serverManager'; +import { SERVER_STARTUP_DELAY_MS } from '../constants'; +import { logger } from '../utils/logger'; export default function Player() { const { id } = useParams<{ id: string }>(); @@ -42,12 +44,12 @@ export default function Player() { setStatus('connecting'); } catch { // Server not running, try to start it - console.log('Server not running, attempting to start...'); + logger.info('Server not running, attempting to start...'); try { const started = await serverManager.startServer(); if (started) { // Wait a bit for server to be ready - await new Promise(resolve => setTimeout(resolve, 2000)); + await new Promise(resolve => setTimeout(resolve, SERVER_STARTUP_DELAY_MS)); // Check again await streamingService.checkHealth(); setStatus('connecting'); @@ -56,7 +58,7 @@ export default function Player() { setStatus('error'); } } catch (err) { - console.error('Error starting server:', err); + logger.error('Error starting server', err); setError('Streaming server is not running. Please try again.'); setStatus('error'); } @@ -133,7 +135,7 @@ export default function Player() { if (is4K) { // For 4K, always use HLS transcoding (browsers can't decode HEVC/H.265 natively) - console.log('[4K] Forcing HLS transcoding for 4K video'); + logger.info('[4K] Forcing HLS transcoding for 4K video'); setStatus('buffering'); // Keep buffering until HLS is ready const attemptHlsStart = async (attempt = 1, maxAttempts = 10) => { @@ -147,9 +149,9 @@ export default function Player() { setHlsUrl(`${getApiUrl()}${hlsResult.playlistUrl}`); setStatus('ready'); addToHistory(movie); - console.log('[4K HLS] Successfully started transcoding'); + logger.info('[4K HLS] Successfully started transcoding'); } else if (hlsResult.status === 'transcoding') { - console.log('[4K HLS] Transcoding started, waiting for completion...'); + logger.debug('[4K HLS] Transcoding started, waiting for completion...'); // Continue polling for HLS ready status setTimeout(() => attemptHlsStart(attempt + 1, maxAttempts), 3000); } @@ -157,10 +159,10 @@ export default function Player() { // If 400 error, video file might not be on disk yet if (e?.response?.status === 400 && attempt < maxAttempts) { const errorMsg = e?.response?.data?.error || e?.message || 'Unknown error'; - console.log(`[4K HLS] Attempt ${attempt}/${maxAttempts} failed: ${errorMsg}, retrying...`); + logger.debug(`[4K HLS] Attempt ${attempt}/${maxAttempts} failed: ${errorMsg}, retrying...`); setTimeout(() => attemptHlsStart(attempt + 1, maxAttempts), 3000); } else { - console.error('[4K HLS] Failed to start transcoding:', e); + logger.error('[4K HLS] Failed to start transcoding', e); setError('Failed to start 4K transcoding. Please try a lower quality.'); setStatus('error'); } @@ -187,18 +189,18 @@ export default function Player() { const hlsResult = await streamingService.startHls(result.sessionId); if (hlsResult.playlistUrl) { setHlsUrl(`${getApiUrl()}${hlsResult.playlistUrl}`); - console.log('[HLS] Successfully started transcoding'); + logger.info('[HLS] Successfully started transcoding'); } else if (hlsResult.status === 'transcoding') { - console.log('[HLS] Transcoding started, waiting for completion...'); + logger.debug('[HLS] Transcoding started, waiting for completion...'); } } catch (e: any) { // If 400 error, video file might not be on disk yet if (e?.response?.status === 400 && attempt < maxAttempts) { const errorMsg = e?.response?.data?.error || e?.message || 'Unknown error'; - console.log(`[HLS] Attempt ${attempt}/${maxAttempts} failed: ${errorMsg}, retrying...`); + logger.debug(`[HLS] Attempt ${attempt}/${maxAttempts} failed: ${errorMsg}, retrying...`); await attemptHlsStart(attempt + 1, maxAttempts); } else { - console.log('[HLS] Not available, using direct stream'); + logger.debug('[HLS] Not available, using direct stream'); } } }; @@ -214,7 +216,7 @@ export default function Player() { setTimeout(pollStatus, 1000); } } catch (e) { - console.error('Status poll error:', e); + logger.error('Status poll error', e); setTimeout(pollStatus, 2000); } }; @@ -222,7 +224,7 @@ export default function Player() { pollStatus(); } catch (err) { - console.error('Stream start error:', err); + logger.error('Stream start error', err); setError(err instanceof Error ? err.message : 'Failed to start stream'); setStatus('error'); } @@ -233,7 +235,9 @@ export default function Player() { // Cleanup on unmount return () => { if (sessionId) { - streamingService.stopStream(sessionId).catch(console.error); + streamingService.stopStream(sessionId).catch((err) => { + logger.error('Error stopping stream', err); + }); streamingService.disconnect(); } }; @@ -264,12 +268,12 @@ export default function Player() { setStatus('connecting'); } catch { // Server not running, try to start it - console.log('Server not running, attempting to start on retry...'); + logger.info('Server not running, attempting to start on retry...'); try { const started = await serverManager.startServer(); if (started) { // Wait a bit for server to be ready - await new Promise(resolve => setTimeout(resolve, 3000)); + await new Promise(resolve => setTimeout(resolve, SERVER_STARTUP_DELAY_MS)); // Check again await streamingService.checkHealth(); setStatus('connecting'); @@ -278,7 +282,7 @@ export default function Player() { setStatus('error'); } } catch (err) { - console.error('Error starting server on retry:', err); + logger.error('Error starting server on retry', err); setError('Streaming server is not running. Please try again.'); setStatus('error'); } diff --git a/src/pages/Search.tsx b/src/pages/Search.tsx index fbb0cee..8e64448 100644 --- a/src/pages/Search.tsx +++ b/src/pages/Search.tsx @@ -1,31 +1,50 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import { useSearchParams } from 'react-router-dom'; import { motion } from 'framer-motion'; import { Search as SearchIcon, X } from 'lucide-react'; import MovieGrid from '../components/movie/MovieGrid'; import Input from '../components/ui/Input'; import { useSearch } from '../hooks/useMovies'; +import { SEARCH_DEBOUNCE_MS } from '../constants'; +import { validateSearchQuery, sanitizeSearchQuery } from '../utils/validation'; export default function Search() { const [searchParams, setSearchParams] = useSearchParams(); const [query, setQuery] = useState(searchParams.get('q') || ''); const [debouncedQuery, setDebouncedQuery] = useState(query); + const [validationError, setValidationError] = useState(null); - const { movies, isLoading, totalCount } = useSearch(debouncedQuery); + // Validate and sanitize query + const sanitizedQuery = useMemo(() => { + if (!query) return ''; + const sanitized = sanitizeSearchQuery(query); + const validation = validateSearchQuery(sanitized); + + if (!validation.valid) { + setValidationError(validation.error || null); + return ''; + } + + setValidationError(null); + return sanitized; + }, [query]); + + const { movies, isLoading, error, totalCount } = useSearch(debouncedQuery); // Debounce search useEffect(() => { const timer = setTimeout(() => { - setDebouncedQuery(query); - if (query) { - setSearchParams({ q: query }); + if (sanitizedQuery) { + setDebouncedQuery(sanitizedQuery); + setSearchParams({ q: sanitizedQuery }); } else { + setDebouncedQuery(''); setSearchParams({}); } - }, 300); + }, SEARCH_DEBOUNCE_MS); return () => clearTimeout(timer); - }, [query, setSearchParams]); + }, [sanitizedQuery, setSearchParams]); return (
- {debouncedQuery && ( + {validationError && ( +

{validationError}

+ )} + {debouncedQuery && !validationError && (

{isLoading ? 'Searching...' + : error + ? `Error: ${error}` : `Found ${totalCount.toLocaleString()} results for "${debouncedQuery}"`}

)} diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 1c85ec2..f4e76fc 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -145,9 +145,12 @@ export default function Settings() { { value: '5', label: '5' }, ]} value={settings.maxConcurrentDownloads.toString()} - onChange={(e) => - updateSettings({ maxConcurrentDownloads: parseInt(e.target.value) }) - } + onChange={(e) => { + const value = parseInt(e.target.value, 10); + if (!isNaN(value) && value > 0 && value <= 10) { + updateSettings({ maxConcurrentDownloads: value }); + } + }} />
diff --git a/src/pages/TVPlayer.tsx b/src/pages/TVPlayer.tsx index 4c3f70c..a07ec57 100644 --- a/src/pages/TVPlayer.tsx +++ b/src/pages/TVPlayer.tsx @@ -36,6 +36,28 @@ export default function TVPlayer() { const poster = searchParams.get('poster') || ''; const backdrop = searchParams.get('backdrop') || ''; + // Extract hash from magnet URL if not provided directly + const torrentHash = hash || extractHashFromMagnet(magnetUrl || ''); + + // Validate torrent data + useEffect(() => { + if (!torrentHash) { + setError('Invalid torrent data - no hash found. This torrent may be corrupted or from an unsupported source.'); + setStatus('error'); + return; + } + if (!magnetUrl && !hash) { + setError('Invalid torrent data - no magnet URL or hash provided'); + setStatus('error'); + return; + } + if (magnetUrl && !magnetUrl.startsWith('magnet:')) { + setError('Invalid magnet URL format'); + setStatus('error'); + return; + } + }, [torrentHash, magnetUrl, hash]); + const [sessionId, setSessionId] = useState(null); const [streamSession, setStreamSession] = useState(null); const [streamUrl, setStreamUrl] = useState(null); @@ -54,8 +76,8 @@ export default function TVPlayer() { poster?: string; } | null>(null); const [nextEpisodeSessionId, setNextEpisodeSessionId] = useState(null); - const [nextEpisodeStreamUrl, setNextEpisodeStreamUrl] = useState(null); - const [nextEpisodeHlsUrl, setNextEpisodeHlsUrl] = useState(null); + const [, setNextEpisodeStreamUrl] = useState(null); + const [, setNextEpisodeHlsUrl] = useState(null); const countdownRef = useRef(null); const videoDurationRef = useRef(0); const videoCurrentTimeRef = useRef(0); @@ -71,9 +93,6 @@ export default function TVPlayer() { backdrop: backdrop ? decodeURIComponent(backdrop) : undefined, }; - // Extract hash from magnet URL if not provided directly - const torrentHash = hash || extractHashFromMagnet(magnetUrl || ''); - // Check server health useEffect(() => { const checkServer = async () => { @@ -99,6 +118,8 @@ export default function TVPlayer() { const episodeName = `${showTitle} S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}${episodeTitle ? ` - ${episodeTitle}` : ''}`; try { + console.log(`[TVPlayer] Starting stream with hash: ${torrentHash}, magnet: ${magnetUrl?.substring(0, 60)}...`); + // Start the stream const result = await streamingService.startStream( torrentHash, @@ -106,16 +127,30 @@ export default function TVPlayer() { quality ); + if (!result) { + throw new Error('Failed to start stream - no response from server'); + } + setSessionId(result.sessionId); + // Get initial status immediately + const initialStatus = await streamingService.getStatus(result.sessionId); + setStreamSession(initialStatus); + // Connect to WebSocket for updates await streamingService.connect(result.sessionId); // Subscribe to updates streamingService.subscribe(result.sessionId, (data: unknown) => { const update = data as { type: string } & Partial; + console.log('[TVPlayer] WebSocket update:', update.type, update); + if (update.type === 'progress') { - setStreamSession(update as StreamSession); + // Merge update with existing session state + setStreamSession(prev => ({ + ...prev, + ...update, + } as StreamSession)); } else if (update.type === 'transcode') { const transcodeUpdate = update as { status?: string; playlistUrl?: string }; if (transcodeUpdate.status === 'ready' && transcodeUpdate.playlistUrl) { @@ -124,11 +159,15 @@ export default function TVPlayer() { } }); - // Poll for status until ready + // Poll for status until ready (backup to WebSocket) const pollStatus = async () => { try { const statusData = await streamingService.getStatus(result.sessionId); - setStreamSession(statusData); + // Always update session state from polling (in case WebSocket misses updates) + setStreamSession(prev => ({ + ...prev, + ...statusData, + })); if (statusData.status === 'ready' || statusData.progress >= 0.05) { // Set direct video URL @@ -205,7 +244,7 @@ export default function TVPlayer() { const episodes = seasonData.episodes || []; // Find current episode index - const currentIndex = episodes.findIndex(e => e.episode_number === episode); + const currentIndex = episodes.findIndex((e: any) => e.episode_number === episode); if (currentIndex === -1 || currentIndex === episodes.length - 1) { // Check if there's a next season diff --git a/src/pages/TVShowDetails.tsx b/src/pages/TVShowDetails.tsx index 74c01d4..c425002 100644 --- a/src/pages/TVShowDetails.tsx +++ b/src/pages/TVShowDetails.tsx @@ -47,10 +47,8 @@ export default function TVShowDetails() { const [show, setShow] = useState<(DiscoveredShow & { seasons: Season[]; externalIds: any }) | null>(null); const [selectedSeason, setSelectedSeason] = useState(1); const [episodes, setEpisodes] = useState([]); - const [torrents, setTorrents] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isLoadingEpisodes, setIsLoadingEpisodes] = useState(false); - const [isLoadingTorrents, setIsLoadingTorrents] = useState(false); const [error, setError] = useState(null); const [showSeasonDropdown, setShowSeasonDropdown] = useState(false); const [streamingEpisode] = useState<{ season: number; episode: number } | null>(null); @@ -61,6 +59,10 @@ export default function TVShowDetails() { episodeTitle: string; } | null>(null); + // Per-episode torrent state (lazy loading) + const [episodeTorrents, setEpisodeTorrents] = useState>({}); + const [loadingEpisodes, setLoadingEpisodes] = useState>(new Set()); + // Load show details useEffect(() => { async function loadShow() { @@ -110,96 +112,60 @@ export default function TVShowDetails() { loadEpisodes(); }, [id, selectedSeason]); - // Load torrents for the show - useEffect(() => { - async function loadTorrents() { - if (!show?.imdbId) { - console.warn('[TVShowDetails] No IMDB ID available for torrent lookup. Show:', show?.title); - // Still try to load with just the title (though it won't work with EZTV) - if (show?.title) { - try { - const torrentData = await tvDiscoveryApi.getTorrents('', show.title); - setTorrents(torrentData); - } catch (err) { - console.error('Error loading torrents without IMDB ID:', err); - } - } - return; - } - - setIsLoadingTorrents(true); - console.log('[TVShowDetails] Loading torrents for IMDB:', show.imdbId, 'Show:', show.title); - - try { - const torrentData = await tvDiscoveryApi.getTorrents(show.imdbId, show.title); - console.log(`[TVShowDetails] Loaded ${torrentData.length} torrents for ${show.title}`); - - if (torrentData.length === 0) { - console.warn(`[TVShowDetails] No torrents found for ${show.title} (IMDB: ${show.imdbId})`); - } - - setTorrents(torrentData); - } catch (err: any) { - console.error('[TVShowDetails] Error loading torrents:', err.response?.data || err.message); - } finally { - setIsLoadingTorrents(false); - } + // Load torrents for a specific episode (lazy loading) + const loadEpisodeTorrents = async (season: number, episode: number) => { + if (!show) { + console.warn('[TVShowDetails] Show data not loaded yet'); + return; + } + const imdbId = (show as any).imdbId; + if (!imdbId) { + console.warn('[TVShowDetails] No IMDB ID available for torrent lookup. Show:', show.title); + return; + } + if (!show.title) { + console.warn('[TVShowDetails] No title available for torrent lookup'); + return; } - - loadTorrents(); - }, [show?.imdbId, show?.title]); - // Episode-specific torrent cache - const [episodeTorrentCache, setEpisodeTorrentCache] = useState>(new Map()); - const [searchingEpisode, setSearchingEpisode] = useState(null); + const key = `${season}-${episode}`; + if (episodeTorrents[key] || loadingEpisodes.has(key)) { + return; // Already loaded or loading + } + + setLoadingEpisodes(prev => new Set(prev).add(key)); + + try { + console.log(`[TVShowDetails] Loading torrents for S${season}E${episode} - IMDB: ${imdbId}`); + const torrents = await tvDiscoveryApi.searchEpisodeTorrents(show.title, season, episode, imdbId); + + setEpisodeTorrents(prev => ({ + ...prev, + [key]: torrents + })); + + console.log(`[TVShowDetails] Found ${torrents.length} torrents for S${season}E${episode}`); + } catch (error) { + console.error(`Error loading torrents for S${season}E${episode}:`, error); + setEpisodeTorrents(prev => ({ + ...prev, + [key]: [] + })); + } finally { + setLoadingEpisodes(prev => { + const newSet = new Set(prev); + newSet.delete(key); + return newSet; + }); + } + }; + + // Episode-specific torrent cache (kept for potential future use) + // const [episodeTorrentCache, setEpisodeTorrentCache] = useState>(new Map()); + // const [searchingEpisode, setSearchingEpisode] = useState(null); // Find torrents for a specific episode (from cache or main torrents) - const getTorrentsForEpisode = (season: number, episode: number): EztvTorrent[] => { - const cacheKey = `${season}-${episode}`; - - // Check cache first - if (episodeTorrentCache.has(cacheKey)) { - return episodeTorrentCache.get(cacheKey) || []; - } - - // Filter from main torrents - return torrents.filter((t) => { - const seasonMatch = t.season === season.toString().padStart(2, '0') || - t.season === season.toString() || - t.title.toLowerCase().includes(`s${season.toString().padStart(2, '0')}`); - const episodeMatch = t.episode === episode.toString().padStart(2, '0') || - t.episode === episode.toString() || - t.title.toLowerCase().includes(`e${episode.toString().padStart(2, '0')}`); - return seasonMatch && episodeMatch; - }).sort((a, b) => { - // Sort by quality (prefer 1080p) - const qualityOrder: Record = { '2160p': 4, '1080p': 3, '720p': 2, '480p': 1, 'Unknown': 0 }; - return (qualityOrder[b.quality] || 0) - (qualityOrder[a.quality] || 0); - }); - }; - // Search for episode-specific torrents - const searchForEpisode = async (season: number, episode: number) => { - if (!show?.title) return; - - const cacheKey = `${season}-${episode}`; - setSearchingEpisode(cacheKey); - - try { - // Pass the IMDB ID for better search results - const results = await tvDiscoveryApi.searchEpisodeTorrents( - show.title, - season, - episode, - show.imdbId - ); - setEpisodeTorrentCache(prev => new Map(prev).set(cacheKey, results)); - } catch (error) { - console.error('Error searching for episode:', error); - } finally { - setSearchingEpisode(null); - } - }; // Modal state for external search const [showSearchModal, setShowSearchModal] = useState<{ @@ -211,40 +177,24 @@ export default function TVShowDetails() { // Handle episode play click - show quality selector if multiple options const handlePlayEpisodeClick = (season: number, episode: number) => { - const episodeTorrents = getTorrentsForEpisode(season, episode); - - if (episodeTorrents.length === 0) { - // No torrents, try searching - searchForEpisode(season, episode); + const episodeKey = `${season}-${episode}`; + const currentEpisodeTorrents = episodeTorrents[episodeKey] || []; + + if (currentEpisodeTorrents.length === 0) { + // No torrents loaded yet, this shouldn't happen since button only shows when torrents exist return; } - - // Check if any torrent is a fallback search URL - const hasFallback = episodeTorrents.some(t => t.magnetUrl.startsWith('https://')); - if (hasFallback && episodeTorrents.length === 1) { - // Only fallback, show search modal - setShowSearchModal({ season, episode, torrent: episodeTorrents[0] }); - return; - } - - // Filter out fallback torrents - const validTorrents = episodeTorrents.filter(t => !t.magnetUrl.startsWith('https://')); - - if (validTorrents.length === 0) { - setShowSearchModal({ season, episode, torrent: episodeTorrents[0] }); - return; - } - + // If only one quality, play directly - if (validTorrents.length === 1) { - handlePlayEpisode(season, episode, validTorrents[0]); + if (currentEpisodeTorrents.length === 1) { + handlePlayEpisode(season, episode, currentEpisodeTorrents[0]); } else { // Show quality selector const episodeInfo = episodes.find(e => e.episode_number === episode); setShowQualityModal({ season, episode, - torrents: validTorrents, + torrents: currentEpisodeTorrents, episodeTitle: episodeInfo?.name || `Episode ${episode}`, }); } @@ -470,12 +420,22 @@ export default function TVShowDetails() {
+ ) : episodes.length === 0 ? ( +
+

No episodes found for this season.

+

Try selecting a different season.

+
) : (
+
+ Found {episodes.length} episodes. Click "Find Streams" on any episode to load torrents. +
{episodes.map((episode) => { - const episodeTorrents = getTorrentsForEpisode(selectedSeason, episode.episode_number); - const hasTorrents = episodeTorrents.length > 0; - const isStreaming = streamingEpisode?.season === selectedSeason && + const episodeKey = `${selectedSeason}-${episode.episode_number}`; + const currentEpisodeTorrents = episodeTorrents[episodeKey] || []; + const hasTorrents = currentEpisodeTorrents.length > 0; + const isLoadingTorrents = loadingEpisodes.has(episodeKey); + const isStreaming = streamingEpisode?.season === selectedSeason && streamingEpisode?.episode === episode.episode_number; return ( @@ -499,7 +459,7 @@ export default function TVShowDetails() {
)}
- {hasTorrents && ( + {hasTorrents ? ( + ) : ( + )}
@@ -548,7 +526,7 @@ export default function TVShowDetails() { {hasTorrents && (
- {episodeTorrents.slice(0, 3).map((torrent, idx) => ( + {currentEpisodeTorrents.slice(0, 3).map((torrent, idx) => ( -
- )}
); })}
)} - - {isLoadingTorrents && ( -
- - Loading stream sources... -
- )}
{/* Search Modal */} @@ -708,7 +655,7 @@ export default function TVShowDetails() {
handleQualitySelect(torrent as EztvTorrent)} title="" />
diff --git a/src/services/api/rarbg.ts b/src/services/api/rarbg.ts index eba052e..266672f 100644 --- a/src/services/api/rarbg.ts +++ b/src/services/api/rarbg.ts @@ -23,15 +23,19 @@ async function getToken(): Promise { } try { + console.log(`[RARBG] Requesting token from: ${RARBG_API_BASE}?get_token=get_token&app_id=beStream`); const response = await axios.get(RARBG_API_BASE, { - params: { get_token: 'get_token' }, + params: { get_token: 'get_token', app_id: 'beStream' }, timeout: 10000, }); + console.log(`[RARBG] Token response status: ${response.status}`); + console.log(`[RARBG] Token response data:`, response.data); + if (response.data?.token) { cachedToken = response.data.token; tokenExpiry = Date.now() + (14 * 60 * 1000); // 14 minutes (safe margin) - return cachedToken; + return cachedToken || ''; } throw new Error('Failed to get token'); diff --git a/src/services/api/tvDiscovery.ts b/src/services/api/tvDiscovery.ts index 7ac448a..fc3733a 100644 --- a/src/services/api/tvDiscovery.ts +++ b/src/services/api/tvDiscovery.ts @@ -1,51 +1,25 @@ import axios from 'axios'; -import { searchByImdbId, searchByKeyword, extractSeasonEpisode, extractQuality as extractRarbgQuality } from './rarbg'; -import type { RarbgTorrent } from './rarbg'; // TMDB API for TV show metadata and discovery // Using a proxy to avoid CORS issues const TMDB_API_KEY = 'feb8e38b40eb61667c6b2ee0ad8a97f4'; -const TMDB_BASE_URL = import.meta.env.DEV ? '/api/tmdb' : 'https://api.themoviedb.org/3'; -const TMDB_IMAGE_BASE = 'https://image.tmdb.org/t/p'; +const TMDB_BASE_URL = 'https://api.themoviedb.org/3'; // Temporarily bypass proxy to test +const TMDB_IMAGE_BASE = 'https://image.tmdb.org/t/p/'; // EZTV API for torrents const EZTV_BASE_URL = '/api/eztv'; // Proxied through Vite -export interface DiscoveredShow { - id: number; - imdbId?: string; - title: string; - originalTitle?: string; - overview: string; - poster: string | null; - backdrop: string | null; - firstAirDate: string; - year: number; - rating: number; - voteCount: number; - popularity: number; - genres: string[]; - status?: string; - seasonCount?: number; - episodeCount?: number; - networks?: string[]; -} +// EZTV authentication (optional - for accessing more content) +// Set these in your environment variables +const EZTV_USERNAME = import.meta.env.VITE_EZTV_USERNAME; +const EZTV_PASSWORD = import.meta.env.VITE_EZTV_PASSWORD; -export interface DiscoveredEpisode { - id: number; - showId: number; - showTitle: string; - season: number; - episode: number; - title: string; - overview: string; - airDate: string; - stillPath: string | null; - torrents: EztvTorrent[]; -} +// Session management for EZTV authentication +let eztvSession: { cookies?: string; lastLogin?: number } = {}; +// Torrent interfaces export interface EztvTorrent { - id: number; + id: string | number; hash: string; filename: string; title: string; @@ -59,80 +33,75 @@ export interface EztvTorrent { magnetUrl: string; } +export interface DiscoveredShow { + id: number; + title: string; + overview: string; + poster: string; + backdrop: string; + firstAirDate: string; + lastAirDate: string; + voteAverage: number; + voteCount: number; + popularity: number; + genres: string[]; + status: string; + seasonCount: number; + episodeCount: number; + networks: string[]; + // Computed fields for compatibility + rating: number; + year: number; +} + // Simple cache -const cache = new Map(); -const CACHE_DURATION = 10 * 60 * 1000; // 10 minutes +const cache = new Map(); +const CACHE_DURATION = 30 * 60 * 1000; // 30 minutes /** - * Convert RARBG torrent to EztvTorrent format + * Login to EZTV for authenticated access (optional) */ -function convertRarbgToEztv(rarbgTorrent: RarbgTorrent): EztvTorrent { - const seInfo = extractSeasonEpisode(rarbgTorrent.title); - const hashMatch = rarbgTorrent.download.match(/btih:([a-fA-F0-9]{40}|[a-zA-Z2-7]{32})/i); - const hash = hashMatch ? hashMatch[1] : ''; - - return { - id: Date.now() + Math.random(), // RARBG doesn't provide IDs - hash: hash, - filename: rarbgTorrent.title, - title: rarbgTorrent.title, - season: seInfo ? seInfo.season.toString().padStart(2, '0') : '0', - episode: seInfo ? seInfo.episode.toString().padStart(2, '0') : '0', - quality: extractRarbgQuality(rarbgTorrent.title), - size: formatBytes(rarbgTorrent.size), - sizeBytes: rarbgTorrent.size, - seeds: rarbgTorrent.seeders, - peers: rarbgTorrent.leechers, - magnetUrl: rarbgTorrent.download, - }; +async function loginToEZTV(): Promise { + // Check if we have credentials + if (!EZTV_USERNAME || !EZTV_PASSWORD) { + console.log('[EZTV] No credentials provided, skipping authentication'); + return false; + } + + // Check if we have a recent session + if (eztvSession.cookies && eztvSession.lastLogin && + Date.now() - eztvSession.lastLogin < 30 * 60 * 1000) { // 30 minutes + console.log('[EZTV] Using existing session'); + return true; + } + + try { + // EZTV login is broken (404), skip for now + console.log('[EZTV] EZTV login is broken, using anonymous access'); + return false; + } catch (error: any) { + console.warn('[EZTV] Login failed:', error.message); + return false; + } } /** - * Deduplicate torrents by hash (preferred) or title + * Transform TMDB show data to our format */ -function deduplicateTorrents(torrents: EztvTorrent[]): EztvTorrent[] { - const seen = new Map(); - - for (const torrent of torrents) { - // Use hash as primary key, fallback to title - const key = torrent.hash || torrent.title.toLowerCase(); - - if (!seen.has(key)) { - seen.set(key, torrent); - } else { - // Keep the one with more seeders - const existing = seen.get(key)!; - if (torrent.seeds > existing.seeds) { - seen.set(key, torrent); - } - } - } - - return Array.from(seen.values()); -} - -async function cachedFetch(key: string, fetcher: () => Promise): Promise { - const cached = cache.get(key); - if (cached && Date.now() - cached.timestamp < CACHE_DURATION) { - return cached.data as T; - } - const data = await fetcher(); - cache.set(key, { data, timestamp: Date.now() }); - return data; -} - -// Transform TMDB show data function transformShow(show: any): DiscoveredShow { + const posterPath = show.poster_path ? `${TMDB_IMAGE_BASE}w342${show.poster_path}` : ''; + const backdropPath = show.backdrop_path ? `${TMDB_IMAGE_BASE}w1280${show.backdrop_path}` : ''; + const year = show.first_air_date ? new Date(show.first_air_date).getFullYear() : 0; + return { id: show.id, - title: show.name || show.original_name, - originalTitle: show.original_name, + title: show.name || show.title, overview: show.overview || '', - poster: show.poster_path ? `${TMDB_IMAGE_BASE}/w500${show.poster_path}` : null, - backdrop: show.backdrop_path ? `${TMDB_IMAGE_BASE}/original${show.backdrop_path}` : null, + poster: posterPath, + backdrop: backdropPath, firstAirDate: show.first_air_date || '', - year: show.first_air_date ? parseInt(show.first_air_date.split('-')[0]) : 0, - rating: show.vote_average || 0, + lastAirDate: show.last_air_date || '', + voteAverage: show.vote_average || 0, voteCount: show.vote_count || 0, popularity: show.popularity || 0, genres: show.genres?.map((g: any) => g.name) || [], @@ -140,9 +109,94 @@ function transformShow(show: any): DiscoveredShow { seasonCount: show.number_of_seasons, episodeCount: show.number_of_episodes, networks: show.networks?.map((n: any) => n.name) || [], + // Computed fields for compatibility + rating: show.vote_average || 0, + year: year, }; } +/** + * Extract quality from torrent title + */ +function extractQuality(title: string): string { + const titleLower = title.toLowerCase(); + + if (titleLower.includes('2160p') || titleLower.includes('4k')) return '2160p'; + if (titleLower.includes('1080p')) return '1080p'; + if (titleLower.includes('720p')) return '720p'; + if (titleLower.includes('480p')) return '480p'; + if (titleLower.includes('360p')) return '360p'; + + return 'Unknown'; +} + +/** + * Deduplicate torrents by hash + */ +function deduplicateTorrents(torrents: EztvTorrent[]): EztvTorrent[] { + const seen = new Set(); + return torrents.filter(torrent => { + if (seen.has(torrent.hash)) { + return false; + } + seen.add(torrent.hash); + return true; + }); +} + +async function cachedFetch(key: string, fetcher: () => Promise): Promise { + const cached = cache.get(key); + if (cached && Date.now() - cached.timestamp < CACHE_DURATION) { + return cached.data; + } + + // Retry logic for network errors + let lastError: any; + for (let attempt = 1; attempt <= 3; attempt++) { + try { + console.log(`[TMDB] Attempting ${key} (attempt ${attempt}/3)`); + const data = await fetcher(); + cache.set(key, { data, timestamp: Date.now() }); + if (attempt > 1) { + console.log(`[TMDB] Success on retry ${attempt} for ${key}`); + } + return data; + } catch (error: any) { + lastError = error; + const isRetryableError = error.code === 'ECONNRESET' || + error.code === 'ETIMEDOUT' || + error.code === 'ENOTFOUND' || + error.code === 'ECONNREFUSED' || + error.message?.includes('socket hang up') || + error.message?.includes('network') || + error.message?.includes('timeout') || + error.message?.includes('disconnected') || + error.message?.includes('TLS'); + + if (!isRetryableError || attempt === 3) { + // Don't retry non-network errors or on final attempt + console.error(`[TMDB] Final failure for ${key} after ${attempt} attempts:`, error.message); + break; + } + + // Wait before retrying (exponential backoff with jitter) + const delay = Math.pow(2, attempt) * 1000 + Math.random() * 1000; + console.log(`[TMDB] Retry ${attempt}/3 for ${key} after ${delay}ms (error: ${error.message})`); + await new Promise(resolve => setTimeout(resolve, delay)); + } + } + + throw lastError; +} + +function formatBytes(bytes: number): string { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; +} + export const tvDiscoveryApi = { /** * Get trending TV shows @@ -160,56 +214,16 @@ export const tvDiscoveryApi = { }, /** - * Get popular TV shows + * Get popular TV shows (alias for trending) */ async getPopular(page = 1): Promise<{ shows: DiscoveredShow[]; totalPages: number }> { - return cachedFetch(`popular:${page}`, async () => { - const response = await axios.get(`${TMDB_BASE_URL}/tv/popular`, { - params: { api_key: TMDB_API_KEY, page }, - }); - return { - shows: response.data.results.map(transformShow), - totalPages: response.data.total_pages, - }; - }); - }, - - /** - * Get top rated TV shows - */ - async getTopRated(page = 1): Promise<{ shows: DiscoveredShow[]; totalPages: number }> { - return cachedFetch(`toprated:${page}`, async () => { - const response = await axios.get(`${TMDB_BASE_URL}/tv/top_rated`, { - params: { api_key: TMDB_API_KEY, page }, - }); - return { - shows: response.data.results.map(transformShow), - totalPages: response.data.total_pages, - }; - }); - }, - - /** - * Get shows airing today - */ - async getAiringToday(page = 1): Promise<{ shows: DiscoveredShow[]; totalPages: number }> { - return cachedFetch(`airing:${page}`, async () => { - const response = await axios.get(`${TMDB_BASE_URL}/tv/airing_today`, { - params: { api_key: TMDB_API_KEY, page }, - }); - return { - shows: response.data.results.map(transformShow), - totalPages: response.data.total_pages, - }; - }); + return this.getTrending(page); }, /** * Search TV shows */ async search(query: string, page = 1): Promise<{ shows: DiscoveredShow[]; totalPages: number }> { - if (!query.trim()) return { shows: [], totalPages: 0 }; - return cachedFetch(`search:${query}:${page}`, async () => { const response = await axios.get(`${TMDB_BASE_URL}/search/tv`, { params: { api_key: TMDB_API_KEY, query, page }, @@ -222,34 +236,78 @@ export const tvDiscoveryApi = { }, /** - * Get show details + * Get TV show details */ - async getShowDetails(tmdbId: number): Promise { - return cachedFetch(`show:${tmdbId}`, async () => { - const [details, externalIds] = await Promise.all([ - axios.get(`${TMDB_BASE_URL}/tv/${tmdbId}`, { + async getShowDetails(showId: number): Promise { + return cachedFetch(`details:${showId}`, async () => { + // First get basic details + const response = await axios.get(`${TMDB_BASE_URL}/tv/${showId}`, { + params: { api_key: TMDB_API_KEY }, + }); + const data = response.data; + + // Get external IDs (includes IMDB ID) + let externalIds = {}; + try { + console.log(`[TMDB] Fetching external IDs for show ${showId}`); + const externalResponse = await axios.get(`${TMDB_BASE_URL}/tv/${showId}/external_ids`, { params: { api_key: TMDB_API_KEY }, - }), - axios.get(`${TMDB_BASE_URL}/tv/${tmdbId}/external_ids`, { - params: { api_key: TMDB_API_KEY }, - }), - ]); - + }); + externalIds = externalResponse.data; + console.log(`[TMDB] External IDs for ${showId}:`, externalIds); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.warn('[TMDB] Could not fetch external IDs for show:', showId, errorMessage); + } + + // Transform to include computed fields that components expect + const posterPath = data.poster_path ? `${TMDB_IMAGE_BASE}w342${data.poster_path}` : ''; + const backdropPath = data.backdrop_path ? `${TMDB_IMAGE_BASE}w1280${data.backdrop_path}` : ''; + const year = data.first_air_date ? new Date(data.first_air_date).getFullYear() : 0; + const imdbId = data.imdb_id || (externalIds && typeof externalIds === 'object' && 'imdb_id' in externalIds ? (externalIds as any).imdb_id : undefined); + + console.log(`[TMDB] Show ${showId} IMDB ID: ${imdbId} (from ${data.imdb_id ? 'details' : 'external_ids'})`); + return { - ...transformShow(details.data), - imdbId: externalIds.data.imdb_id, - seasons: details.data.seasons || [], - externalIds: externalIds.data, + ...data, + poster: posterPath, + backdrop: backdropPath, + rating: data.vote_average || 0, + year: year, + genres: data.genres?.map((g: any) => g.name) || [], + networks: data.networks?.map((n: any) => n.name) || [], + imdbId: imdbId, + title: data.name || data.title, // TMDB uses 'name' for TV shows }; }); }, /** - * Get season details + * Get TV show seasons */ - async getSeasonDetails(tmdbId: number, seasonNumber: number): Promise { - return cachedFetch(`season:${tmdbId}:${seasonNumber}`, async () => { - const response = await axios.get(`${TMDB_BASE_URL}/tv/${tmdbId}/season/${seasonNumber}`, { + async getSeasons(showId: number): Promise { + const details = await this.getShowDetails(showId); + return details.seasons || []; + }, + + /** + * Get season episodes + */ + async getEpisodes(showId: number, seasonNumber: number): Promise { + return cachedFetch(`episodes:${showId}:${seasonNumber}`, async () => { + const response = await axios.get(`${TMDB_BASE_URL}/tv/${showId}/season/${seasonNumber}`, { + params: { api_key: TMDB_API_KEY }, + }); + return response.data.episodes || []; + }); + }, + + /** + * Get season details (alias for getEpisodes for backward compatibility) + */ + async getSeasonDetails(showId: number, seasonNumber: number): Promise { + return cachedFetch(`season:${showId}:${seasonNumber}`, async () => { + const response = await axios.get(`${TMDB_BASE_URL}/tv/${showId}/season/${seasonNumber}`, { params: { api_key: TMDB_API_KEY }, }); return response.data; @@ -257,131 +315,142 @@ export const tvDiscoveryApi = { }, /** - * Get torrents for a show from multiple sources - * Strategy: Try RARBG first (better quality/reliability), fallback to EZTV + * Get torrents for a TV show */ - async getTorrents(imdbId: string, showTitle?: string): Promise { + async getTorrents(imdbId?: string, _showTitle?: string): Promise { const torrents: EztvTorrent[] = []; - + if (!imdbId) { console.warn('[Torrents] No IMDB ID provided for torrent lookup'); return torrents; } - - // Try RARBG first (primary source) + try { - console.log(`[RARBG] Fetching torrents for IMDB ID: ${imdbId}`); - const rarbgResults = await searchByImdbId(imdbId, 500); - - if (rarbgResults.length > 0) { - const converted = rarbgResults.map(convertRarbgToEztv); - torrents.push(...converted); - console.log(`[RARBG] Successfully loaded ${converted.length} torrents`); + const isAuthenticated = await loginToEZTV(); + const numericId = imdbId.replace('tt', ''); + console.log(`[EZTV] Fetching torrents for IMDB ID: ${numericId} (original: ${imdbId}) ${isAuthenticated ? '(authenticated)' : '(anonymous)'}`); + + // Use the working eztvx.to API + const response = await axios.get(`${EZTV_BASE_URL}/get-torrents`, { + params: { + imdb_id: numericId, + limit: 100, + }, + timeout: 15000, + validateStatus: (status) => status < 500, + }); + + console.log(`[EZTV] Response status: ${response.status}`); + + if (response.data?.torrents && Array.isArray(response.data.torrents)) { + const torrentsData = response.data.torrents; + console.log(`[EZTV] Found ${torrentsData.length} torrents for IMDB ${numericId}`); + + torrents.push(...torrentsData.map((t: any) => ({ + id: t.id || Date.now() + Math.random(), + hash: t.hash || t.info_hash || '', + filename: t.filename || t.title || '', + title: t.title || t.filename || '', + season: t.season?.toString() || '0', + episode: t.episode?.toString() || '0', + quality: extractQuality(t.title || t.filename || ''), + size: t.size_bytes ? formatBytes(parseInt(t.size_bytes)) : (t.size || 'Unknown'), + sizeBytes: parseInt(t.size_bytes || t.size || '0'), + seeds: parseInt(t.seeds || '0'), + peers: parseInt(t.peers || '0'), + magnetUrl: t.magnet_url || t.magnet || '', + }))); + + console.log(`[EZTV] Successfully loaded ${torrents.length} torrents`); } else { - console.log(`[RARBG] No torrents found for IMDB ID: ${imdbId}`); + console.log(`[EZTV] No torrents found for IMDB ID: ${numericId}`); } + + // Note: lastError tracking removed - errors are logged above } catch (error: any) { - console.error('[RARBG] API error:', error?.message || error); + console.error('[EZTV] API error:', error.response?.data || error.message); } - - // Fallback to EZTV if RARBG had no results or failed - if (torrents.length === 0) { - try { - const numericId = imdbId.replace('tt', ''); - console.log(`[EZTV] Fallback: Fetching torrents for IMDB ID: ${numericId} (original: ${imdbId})`); - - // Try multiple limit values - some shows have many episodes - const limits = [500, 250, 100]; - let lastError: any = null; - - for (const limit of limits) { - try { - const response = await axios.get(`${EZTV_BASE_URL}/get-torrents`, { - params: { - imdb_id: numericId, - limit: limit - }, - timeout: 15000, - validateStatus: (status) => status < 500, // Don't throw on 4xx errors - }); - - console.log(`[EZTV] Response status: ${response.status}`); - - // Handle different response formats - let torrentsData: any[] = []; - if (response.data?.torrents) { - torrentsData = Array.isArray(response.data.torrents) - ? response.data.torrents - : []; - } else if (Array.isArray(response.data)) { - torrentsData = response.data; - } - - if (torrentsData.length > 0) { - torrents.push(...torrentsData.map((t: any) => ({ - id: t.id || Date.now() + Math.random(), - hash: t.hash || t.info_hash || '', - filename: t.filename || t.title || '', - title: t.title || t.filename || '', - season: t.season?.toString() || '0', - episode: t.episode?.toString() || '0', - quality: extractQuality(t.title || t.filename || ''), - size: t.size_bytes ? formatBytes(parseInt(t.size_bytes)) : (t.size || 'Unknown'), - sizeBytes: parseInt(t.size_bytes || t.size || '0'), - seeds: parseInt(t.seeds || '0'), - peers: parseInt(t.peers || '0'), - magnetUrl: t.magnet_url || t.magnet || '', - }))); - - console.log(`[EZTV] Successfully loaded ${torrents.length} torrents`); - break; // Success, no need to try other limits - } else if (response.status === 200) { - console.log(`[EZTV] No torrents found for IMDB ID: ${numericId}`); - break; // Got a valid response but no torrents - } - } catch (error: any) { - lastError = error; - console.warn(`[EZTV] Error with limit ${limit}:`, error.response?.status || error.message); - // Continue to next limit if this one failed - if (error.response?.status === 404 || error.response?.status === 400) { - break; // Don't retry with different limits for client errors - } - } - } - - if (torrents.length === 0 && lastError) { - console.warn('[EZTV] Failed to fetch torrents:', lastError.response?.data || lastError.message); - } - } catch (error: any) { - console.error('[EZTV] API error:', error.response?.data || error.message); + + const deduplicated = deduplicateTorrents(torrents); + const sorted = deduplicated.sort((a, b) => { + if (b.seeds !== a.seeds) { + return b.seeds - a.seeds; } - } else { - // RARBG had results, but also try EZTV to merge results - try { - const numericId = imdbId.replace('tt', ''); - console.log(`[EZTV] Merging: Fetching additional torrents for IMDB ID: ${numericId}`); - - const response = await axios.get(`${EZTV_BASE_URL}/get-torrents`, { - params: { imdb_id: numericId, limit: 100 }, - timeout: 10000, - validateStatus: (status) => status < 500, - }); - - let torrentsData: any[] = []; - if (response.data?.torrents) { - torrentsData = Array.isArray(response.data.torrents) ? response.data.torrents : []; - } else if (Array.isArray(response.data)) { - torrentsData = response.data; - } - - if (torrentsData.length > 0) { - const eztvTorrents = torrentsData.map((t: any) => ({ + const qualityOrder: Record = { '2160p': 4, '1080p': 3, '720p': 2, '480p': 1, 'Unknown': 0 }; + return (qualityOrder[b.quality] || 0) - (qualityOrder[a.quality] || 0); + }); + + console.log(`[Torrents] Final result: ${sorted.length} unique torrents (from ${torrents.length} total)`); + if (sorted.length === 0 && imdbId) { + console.log(`[Torrents] No torrents found for IMDB ID: ${imdbId}. EZTV may not have this show.`); + } + return sorted; + }, + + /** + * Search for episode torrents using EZTV API + */ + async searchEpisodeTorrents( + _showTitle: string, + season: number, + episode: number, + imdbId?: string + ): Promise { + if (!imdbId) { + console.warn('[Episode Search] No IMDB ID provided'); + return []; + } + + const seasonStr = season.toString().padStart(2, '0'); + const episodeStr = episode.toString().padStart(2, '0'); + const numericId = imdbId.replace('tt', ''); + + console.log(`[Episode Search] Searching for S${seasonStr}E${episodeStr} with IMDB: ${numericId}`); + + try { + const response = await axios.get(`${EZTV_BASE_URL}/get-torrents`, { + params: { + imdb_id: numericId, + limit: 100, // Get plenty of results + }, + timeout: 15000, + validateStatus: (status) => status < 500, + }); + + if (response.data?.torrents && Array.isArray(response.data.torrents)) { + console.log(`[Episode Search] Found ${response.data.torrents.length} total torrents for IMDB ${numericId}`); + + + const filtered = response.data.torrents + .filter((t: any) => { + if (!t.title) return false; + + // Check if season/episode fields exist and match + const torrentSeason = t.season?.toString(); + const torrentEpisode = t.episode?.toString(); + + // EZTV returns season as "1" not "01", so compare normalized values + const seasonMatches = parseInt(torrentSeason || '0') === season; + const episodeMatches = parseInt(torrentEpisode || '0') === episode; + + // Also try title-based matching as fallback + let titleMatches = false; + if (!seasonMatches || !episodeMatches) { + const titleLower = t.title.toLowerCase(); + const seasonPattern = new RegExp(`s0?${season}\\b`, 'i'); + const episodePattern = new RegExp(`e0?${episode}\\b`, 'i'); + titleMatches = seasonPattern.test(titleLower) && episodePattern.test(titleLower); + } + + return (seasonMatches && episodeMatches) || titleMatches; + }) + .map((t: any) => ({ id: t.id || Date.now() + Math.random(), hash: t.hash || t.info_hash || '', filename: t.filename || t.title || '', title: t.title || t.filename || '', - season: t.season?.toString() || '0', - episode: t.episode?.toString() || '0', + season: seasonStr, + episode: episodeStr, quality: extractQuality(t.title || t.filename || ''), size: t.size_bytes ? formatBytes(parseInt(t.size_bytes)) : (t.size || 'Unknown'), sizeBytes: parseInt(t.size_bytes || t.size || '0'), @@ -389,267 +458,17 @@ export const tvDiscoveryApi = { peers: parseInt(t.peers || '0'), magnetUrl: t.magnet_url || t.magnet || '', })); - - torrents.push(...eztvTorrents); - console.log(`[EZTV] Merged ${eztvTorrents.length} additional torrents`); - } - } catch (error: any) { - console.warn('[EZTV] Error merging torrents:', error.message); - } - } - - // Deduplicate and sort by seeders/quality - const deduplicated = deduplicateTorrents(torrents); - const sorted = deduplicated.sort((a, b) => { - // First sort by seeders (descending) - if (b.seeds !== a.seeds) { - return b.seeds - a.seeds; - } - // Then by quality - const qualityOrder: Record = { '2160p': 4, '1080p': 3, '720p': 2, '480p': 1, 'Unknown': 0 }; - return (qualityOrder[b.quality] || 0) - (qualityOrder[a.quality] || 0); - }); - - console.log(`[Torrents] Final result: ${sorted.length} unique torrents (from ${torrents.length} total)`); - return sorted; - }, - /** - * Search for episode torrents using IMDB ID lookup - * Strategy: Try RARBG first, fallback to EZTV - */ - async searchEpisodeTorrents( - showTitle: string, - season: number, - episode: number, - imdbId?: string - ): Promise { - const results: EztvTorrent[] = []; - const seasonStr = season.toString().padStart(2, '0'); - const episodeStr = episode.toString().padStart(2, '0'); - const searchQuery = `${showTitle} S${seasonStr}E${episodeStr}`; - - if (!imdbId) { - console.warn(`[Episode Search] No IMDB ID provided for: ${searchQuery}`); - return results; - } - - // Try RARBG first (primary source) - try { - console.log(`[RARBG] Searching for episode S${seasonStr}E${episodeStr} with IMDB: ${imdbId}`); - const rarbgResults = await searchByImdbId(imdbId, 500); - - if (rarbgResults.length > 0) { - // Filter for the specific episode - const filtered = rarbgResults - .filter((t: RarbgTorrent) => { - const seInfo = extractSeasonEpisode(t.title); - if (seInfo) { - return seInfo.season === season && seInfo.episode === episode; - } - // Fallback: check title for S01E01 pattern - const title = t.title.toLowerCase(); - const seasonPattern = new RegExp(`s0?${season}\\b`, 'i'); - const episodePattern = new RegExp(`e0?${episode}\\b`, 'i'); - return seasonPattern.test(title) && episodePattern.test(title); - }) - .map(convertRarbgToEztv); - - results.push(...filtered); - console.log(`[RARBG] Found ${filtered.length} torrents for S${seasonStr}E${episodeStr}`); + console.log(`[Episode Search] Filtered to ${filtered.length} episodes for S${seasonStr}E${episodeStr}`); + return filtered; } else { - console.log(`[RARBG] No torrents found for IMDB: ${imdbId}`); + console.log(`[Episode Search] No torrents found for IMDB ${numericId} (show may not be on EZTV)`); + return []; } } catch (error: any) { - console.error('[RARBG] Episode search error:', error?.message || error); + console.warn('[Episode Search] EZTV API error:', error.message); + return []; } - - // Fallback to EZTV if RARBG had no results - if (results.length === 0) { - try { - const numericId = imdbId.replace('tt', ''); - console.log(`[EZTV] Searching for episode S${seasonStr}E${episodeStr} with IMDB: ${numericId}`); - - // Try with higher limits to find the episode - const limits = [500, 250, 100]; - let torrentsData: any[] = []; - - for (const limit of limits) { - try { - const response = await axios.get(`${EZTV_BASE_URL}/get-torrents`, { - params: { - imdb_id: numericId, - limit: limit, - }, - timeout: 15000, - validateStatus: (status) => status < 500, - }); - - // Handle different response formats - if (response.data?.torrents) { - torrentsData = Array.isArray(response.data.torrents) - ? response.data.torrents - : []; - } else if (Array.isArray(response.data)) { - torrentsData = response.data; - } - - if (torrentsData.length > 0) { - console.log(`[EZTV] Found ${torrentsData.length} total torrents, filtering for S${seasonStr}E${episodeStr}`); - break; - } - } catch (error: any) { - if (error.response?.status === 404 || error.response?.status === 400) { - break; // Don't retry for client errors - } - console.warn(`[EZTV] Error with limit ${limit}:`, error.message); - } - } - - if (torrentsData.length > 0) { - // Filter for the specific episode - const filtered = torrentsData - .filter((t: any) => { - const title = (t.title || t.filename || '').toLowerCase(); - - // EZTV uses numeric season/episode fields - const torrentSeason = parseInt(t.season) || 0; - const torrentEpisode = parseInt(t.episode) || 0; - - // Check by numeric fields first (most reliable) - if (torrentSeason === season && torrentEpisode === episode) { - return true; - } - - // Fallback: Check for S01E01 format in title (more flexible) - const seasonPattern = new RegExp(`s0?${season}\\b`, 'i'); - const episodePattern = new RegExp(`e0?${episode}\\b`, 'i'); - - return seasonPattern.test(title) && episodePattern.test(title); - }) - .map((t: any) => ({ - id: t.id || Date.now() + Math.random(), - hash: t.hash || t.info_hash || '', - filename: t.filename || t.title || '', - title: t.title || t.filename || '', - season: seasonStr, - episode: episodeStr, - quality: extractQuality(t.title || t.filename || ''), - size: t.size_bytes ? formatBytes(parseInt(t.size_bytes)) : (t.size || 'Unknown'), - sizeBytes: parseInt(t.size_bytes || t.size || '0'), - seeds: parseInt(t.seeds || '0'), - peers: parseInt(t.peers || '0'), - magnetUrl: t.magnet_url || t.magnet || '', - })); - results.push(...filtered); - console.log(`[EZTV] Filtered ${filtered.length} torrents for episode S${seasonStr}E${episodeStr}`); - } else { - console.log(`[EZTV] No torrents found for IMDB ID: ${numericId}`); - } - } catch (error: any) { - console.error('[EZTV] Episode search error:', error.response?.data || error.message); - } - } else { - // RARBG had results, but also try EZTV to merge results - try { - const numericId = imdbId.replace('tt', ''); - console.log(`[EZTV] Merging: Fetching additional episode torrents for IMDB: ${numericId}`); - - const response = await axios.get(`${EZTV_BASE_URL}/get-torrents`, { - params: { imdb_id: numericId, limit: 100 }, - timeout: 10000, - validateStatus: (status) => status < 500, - }); - - let torrentsData: any[] = []; - if (response.data?.torrents) { - torrentsData = Array.isArray(response.data.torrents) ? response.data.torrents : []; - } else if (Array.isArray(response.data)) { - torrentsData = response.data; - } - - if (torrentsData.length > 0) { - const filtered = torrentsData - .filter((t: any) => { - const title = (t.title || t.filename || '').toLowerCase(); - const torrentSeason = parseInt(t.season) || 0; - const torrentEpisode = parseInt(t.episode) || 0; - - if (torrentSeason === season && torrentEpisode === episode) { - return true; - } - - const seasonPattern = new RegExp(`s0?${season}\\b`, 'i'); - const episodePattern = new RegExp(`e0?${episode}\\b`, 'i'); - return seasonPattern.test(title) && episodePattern.test(title); - }) - .map((t: any) => ({ - id: t.id || Date.now() + Math.random(), - hash: t.hash || t.info_hash || '', - filename: t.filename || t.title || '', - title: t.title || t.filename || '', - season: seasonStr, - episode: episodeStr, - quality: extractQuality(t.title || t.filename || ''), - size: t.size_bytes ? formatBytes(parseInt(t.size_bytes)) : (t.size || 'Unknown'), - sizeBytes: parseInt(t.size_bytes || t.size || '0'), - seeds: parseInt(t.seeds || '0'), - peers: parseInt(t.peers || '0'), - magnetUrl: t.magnet_url || t.magnet || '', - })); - - results.push(...filtered); - console.log(`[EZTV] Merged ${filtered.length} additional episode torrents`); - } - } catch (error: any) { - console.warn('[EZTV] Error merging episode torrents:', error.message); - } - } - - // Deduplicate and sort by seeders/quality - const deduplicated = deduplicateTorrents(results); - const sorted = deduplicated.sort((a, b) => { - // First sort by seeders (descending) - if (b.seeds !== a.seeds) { - return b.seeds - a.seeds; - } - // Then by quality - const qualityOrder: Record = { '2160p': 4, '1080p': 3, '720p': 2, '480p': 1, 'Unknown': 0 }; - return (qualityOrder[b.quality] || 0) - (qualityOrder[a.quality] || 0); - }); - - // If no results from either source, generate fallback search links - if (sorted.length === 0) { - console.log(`[Episode Search] No results from RARBG or EZTV, generating fallback for: ${searchQuery}`); - - // Generate placeholder entries that link to torrent search pages - const qualities = ['1080p', '720p', '480p']; - const cleanTitle = showTitle.replace(/[^a-zA-Z0-9 ]/g, '').replace(/\s+/g, '.'); - - qualities.forEach((quality, idx) => { - const title = `${cleanTitle}.S${seasonStr}E${episodeStr}.${quality}.WEB-DL`; - // Create a deterministic hash for UI purposes - const hash = btoa(`${showTitle}${season}${episode}${quality}`).substring(0, 40); - - sorted.push({ - id: Date.now() + idx, - hash: hash, - filename: `${title}.mkv`, - title: title, - season: seasonStr, - episode: episodeStr, - quality: quality, - size: quality === '1080p' ? '~2GB' : quality === '720p' ? '~1GB' : '~500MB', - sizeBytes: quality === '1080p' ? 2147483648 : quality === '720p' ? 1073741824 : 536870912, - seeds: 0, - peers: 0, - magnetUrl: generateSearchUrl(showTitle, season, episode, quality), - }); - }); - } - - console.log(`[Episode Search] Final result: ${sorted.length} unique torrents for S${seasonStr}E${episodeStr}`); - return sorted; }, /** @@ -672,6 +491,36 @@ export const tvDiscoveryApi = { }); }, + /** + * Get top rated TV shows + */ + async getTopRated(page = 1): Promise<{ shows: DiscoveredShow[]; totalPages: number }> { + return cachedFetch(`top-rated:${page}`, async () => { + const response = await axios.get(`${TMDB_BASE_URL}/tv/top_rated`, { + params: { api_key: TMDB_API_KEY, page }, + }); + return { + shows: response.data.results.map(transformShow), + totalPages: response.data.total_pages, + }; + }); + }, + + /** + * Get TV shows airing today + */ + async getAiringToday(page = 1): Promise<{ shows: DiscoveredShow[]; totalPages: number }> { + return cachedFetch(`airing-today:${page}`, async () => { + const response = await axios.get(`${TMDB_BASE_URL}/tv/airing_today`, { + params: { api_key: TMDB_API_KEY, page }, + }); + return { + shows: response.data.results.map(transformShow), + totalPages: response.data.total_pages, + }; + }); + }, + /** * Get TV genres */ @@ -683,46 +532,4 @@ export const tvDiscoveryApi = { return response.data.genres; }); }, - - /** - * Clear cache - */ - clearCache(): void { - cache.clear(); - }, -}; - -// Helper functions -function extractQuality(title: string): string { - const qualityPatterns = [ - { regex: /2160p|4k|uhd/i, quality: '2160p' }, - { regex: /1080p|fhd/i, quality: '1080p' }, - { regex: /720p|hd/i, quality: '720p' }, - { regex: /480p|sd/i, quality: '480p' }, - ]; - - for (const { regex, quality } of qualityPatterns) { - if (regex.test(title)) return quality; - } - return 'Unknown'; -} - -function formatBytes(bytes: number): string { - if (bytes === 0) return '0 Bytes'; - const k = 1024; - const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; -} - -function generateSearchUrl(showTitle: string, season: number, episode: number, quality: string): string { - // Generate a search URL that can be opened in a torrent search engine - const query = `${showTitle} S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')} ${quality}`; - const encoded = encodeURIComponent(query); - // Return a 1337x search URL as fallback - return `https://1337x.to/search/${encoded}/1/`; -} - -export default tvDiscoveryApi; - - +}; \ No newline at end of file diff --git a/src/services/api/yts.ts b/src/services/api/yts.ts index 896977d..136cd88 100644 --- a/src/services/api/yts.ts +++ b/src/services/api/yts.ts @@ -16,15 +16,16 @@ const BASE_URL = isDev ? '/api/yts' : 'https://yts.lt/api/v2'; // Create axios instance with caching const api: AxiosInstance = axios.create({ baseURL: BASE_URL, - timeout: 15000, + timeout: API_TIMEOUT_MS, headers: { 'Content-Type': 'application/json', }, }); +import { CACHE_DURATION_MS, API_TIMEOUT_MS } from '../../constants'; + // Simple in-memory cache const cache = new Map(); -const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes function getCacheKey(endpoint: string, params?: object): string { return `${endpoint}:${JSON.stringify(params || {})}`; @@ -37,7 +38,7 @@ async function cachedRequest( const cacheKey = getCacheKey(endpoint, params); const cached = cache.get(cacheKey); - if (cached && Date.now() - cached.timestamp < CACHE_DURATION) { + if (cached && Date.now() - cached.timestamp < CACHE_DURATION_MS) { return cached.data as T; } diff --git a/src/services/server/serverManager.ts b/src/services/server/serverManager.ts index 9a47eb0..19211e0 100644 --- a/src/services/server/serverManager.ts +++ b/src/services/server/serverManager.ts @@ -1,11 +1,18 @@ import { isCapacitor } from '../../utils/platform'; import streamingService from '../streaming/streamingService'; +import { + SERVER_HEALTH_CHECK_INTERVAL_MS, + SERVER_MAX_STARTUP_RETRIES, + SERVER_WAIT_TIMEOUT_MS, + SERVER_STARTUP_DELAY_MS, +} from '../../constants'; +import { logger } from '../../utils/logger'; // Get NodeJS plugin from Capacitor runtime // Capacitor plugins are accessed through Capacitor.Plugins at runtime const getNodeJS = () => { if (!isCapacitor()) { - console.log('Not a Capacitor platform, NodeJS plugin not needed'); + logger.debug('Not a Capacitor platform, NodeJS plugin not needed'); return null; } @@ -15,25 +22,25 @@ const getNodeJS = () => { // @ts-ignore - CapacitorNodeJS is registered at runtime const Capacitor = (window as any).Capacitor; if (!Capacitor || !Capacitor.Plugins) { - console.error('Capacitor or Capacitor.Plugins not available'); + logger.error('Capacitor or Capacitor.Plugins not available'); return null; } const NodeJS = Capacitor.Plugins.CapacitorNodeJS; if (!NodeJS) { - console.error('NodeJS plugin not found in Capacitor.Plugins'); - console.log('Available plugins:', Object.keys(Capacitor.Plugins || {})); + logger.error('NodeJS plugin not found in Capacitor.Plugins', { + availablePlugins: Object.keys(Capacitor.Plugins || {}), + }); return null; } - console.log('NodeJS plugin found in Capacitor.Plugins'); + logger.debug('NodeJS plugin found in Capacitor.Plugins'); return NodeJS; } catch (error) { - console.error('Failed to access NodeJS plugin:', error); - console.error('Error details:', { + logger.error('Failed to access NodeJS plugin', error, { message: error instanceof Error ? error.message : String(error), - stack: error instanceof Error ? error.stack : undefined + stack: error instanceof Error ? error.stack : undefined, }); return null; } @@ -48,8 +55,8 @@ class ServerManager { private isStarting = false; private isRunning = false; private healthCheckInterval: NodeJS.Timeout | null = null; - private readonly HEALTH_CHECK_INTERVAL = 5000; // 5 seconds - private readonly MAX_STARTUP_RETRIES = 3; + private readonly HEALTH_CHECK_INTERVAL = SERVER_HEALTH_CHECK_INTERVAL_MS; + private readonly MAX_STARTUP_RETRIES = SERVER_MAX_STARTUP_RETRIES; private startupRetries = 0; /** @@ -75,11 +82,11 @@ class ServerManager { // Get NodeJS plugin from Capacitor runtime const NodeJS = getNodeJS(); if (!NodeJS) { - console.error('NodeJS plugin not available - is capacitor-nodejs installed?'); + logger.error('NodeJS plugin not available - is capacitor-nodejs installed?'); throw new Error('NodeJS plugin not available'); } - console.log('NodeJS plugin found, starting runtime...'); + logger.info('NodeJS plugin found, starting runtime...'); // Start Node.js runtime using Capacitor NodeJS plugin // If it's already started, that's fine - we'll just wait for it to be ready @@ -92,24 +99,24 @@ class ServerManager { NODE_ENV: 'production' } }); - console.log('NodeJS.start() called successfully'); + logger.debug('NodeJS.start() called successfully'); } catch (startError: any) { // If the engine is already started, that's okay - just continue if (startError?.message?.includes('already been started')) { - console.log('Node.js engine already started, continuing...'); + logger.debug('Node.js engine already started, continuing...'); } else { - console.error('Error calling NodeJS.start():', startError); + logger.error('Error calling NodeJS.start()', startError); throw startError; } } // Wait for Node.js runtime to be ready - console.log('Waiting for Node.js runtime to be ready...'); + logger.debug('Waiting for Node.js runtime to be ready...'); try { await NodeJS.whenReady(); - console.log('Node.js runtime is ready - server should be starting...'); + logger.info('Node.js runtime is ready - server should be starting...'); } catch (readyError) { - console.error('Error waiting for Node.js runtime:', readyError); + logger.error('Error waiting for Node.js runtime', readyError); throw readyError; } @@ -118,8 +125,8 @@ class ServerManager { try { await this.waitForServer(); this.isRunning = true; - } catch (error) { - console.warn('Server not ready yet, will retry on first use:', error); + } catch (error: unknown) { + logger.warn('Server not ready yet, will retry on first use', error as any); this.isRunning = false; } @@ -128,14 +135,14 @@ class ServerManager { return true; } catch (error) { - console.error('Failed to start server:', error); + logger.error('Failed to start server', error); this.isStarting = false; // Retry if we haven't exceeded max retries if (this.startupRetries < this.MAX_STARTUP_RETRIES) { this.startupRetries++; - console.log(`Retrying server startup (attempt ${this.startupRetries}/${this.MAX_STARTUP_RETRIES})...`); - await new Promise(resolve => setTimeout(resolve, 2000)); + logger.info(`Retrying server startup (attempt ${this.startupRetries}/${this.MAX_STARTUP_RETRIES})...`); + await new Promise(resolve => setTimeout(resolve, SERVER_STARTUP_DELAY_MS)); return this.startServer(); } @@ -158,7 +165,7 @@ class ServerManager { // Note: Capacitor NodeJS doesn't support stopping/restarting // The runtime will be stopped when the app is closed - console.log('Server stop requested (runtime will continue until app closes)'); + logger.info('Server stop requested (runtime will continue until app closes)'); } /** @@ -171,7 +178,7 @@ class ServerManager { /** * Wait for server to be ready */ - private async waitForServer(maxWaitTime = 10000): Promise { + private async waitForServer(maxWaitTime = SERVER_WAIT_TIMEOUT_MS): Promise { const startTime = Date.now(); while (Date.now() - startTime < maxWaitTime) { @@ -186,7 +193,7 @@ class ServerManager { // Don't throw error - server might start later // Just log a warning and continue - console.warn('Server not ready yet, but continuing...'); + logger.warn('Server not ready yet, but continuing...'); } /** @@ -220,7 +227,7 @@ class ServerManager { this.healthCheckInterval = setInterval(async () => { const isHealthy = await this.checkServerHealth(); if (!isHealthy && isCapacitor()) { - console.warn('Server health check failed, attempting restart...'); + logger.warn('Server health check failed, attempting restart...'); // Attempt to restart server this.isRunning = false; await this.startServer(); diff --git a/src/services/streaming/streamingService.ts b/src/services/streaming/streamingService.ts index 16f38ba..08a765e 100644 --- a/src/services/streaming/streamingService.ts +++ b/src/services/streaming/streamingService.ts @@ -1,5 +1,11 @@ import axios from 'axios'; import { getApiUrl } from '../../utils/platform'; +import { logger } from '../../utils/logger'; +import { + MAX_RECONNECT_ATTEMPTS, + RECONNECT_BASE_DELAY_MS, + WEBSOCKET_SUBSCRIBE_DELAY_MS, +} from '../../constants'; // Get API URL using platform-aware resolution // Defaults to http://localhost:3001 for both Electron and Android @@ -33,7 +39,7 @@ export interface TranscodeProgress { class StreamingService { private ws: WebSocket | null = null; private reconnectAttempts = 0; - private maxReconnectAttempts = 5; + private maxReconnectAttempts = MAX_RECONNECT_ATTEMPTS; private listeners: Map void>> = new Map(); /** @@ -47,14 +53,14 @@ class StreamingService { this.ws = new WebSocket(wsUrl); this.ws.onopen = () => { - console.log('WebSocket connected'); + logger.info('WebSocket connected', { sessionId }); this.reconnectAttempts = 0; // Subscribe to session updates - wait a bit to ensure WebSocket is fully open setTimeout(() => { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify({ type: 'subscribe', sessionId })); } - }, 100); + }, WEBSOCKET_SUBSCRIBE_DELAY_MS); resolve(); }; @@ -63,16 +69,16 @@ class StreamingService { const data = JSON.parse(event.data); this.emit(sessionId, data); } catch (e) { - console.error('WebSocket message parse error:', e); + logger.error('WebSocket message parse error', e); } }; this.ws.onerror = (error) => { - console.error('WebSocket error:', error); + logger.error('WebSocket error', error); }; this.ws.onclose = () => { - console.log('WebSocket disconnected'); + logger.info('WebSocket disconnected', { sessionId }); this.attemptReconnect(sessionId); }; } catch (error) { @@ -87,8 +93,10 @@ class StreamingService { private attemptReconnect(sessionId: string) { if (this.reconnectAttempts < this.maxReconnectAttempts) { this.reconnectAttempts++; - console.log(`Reconnecting... attempt ${this.reconnectAttempts}`); - setTimeout(() => this.connect(sessionId), 2000 * this.reconnectAttempts); + logger.info(`Reconnecting... attempt ${this.reconnectAttempts}`, { sessionId }); + setTimeout(() => this.connect(sessionId), RECONNECT_BASE_DELAY_MS * this.reconnectAttempts); + } else { + logger.warn('Max reconnection attempts reached', { sessionId, attempts: this.reconnectAttempts }); } } @@ -119,6 +127,7 @@ class StreamingService { */ disconnect() { if (this.ws) { + logger.debug('Disconnecting WebSocket'); this.ws.close(); this.ws = null; } diff --git a/src/utils/logger.ts b/src/utils/logger.ts new file mode 100644 index 0000000..86907af --- /dev/null +++ b/src/utils/logger.ts @@ -0,0 +1,78 @@ +/** + * Structured logging utility + * Provides consistent logging with levels and environment awareness + */ + +type LogLevel = 'debug' | 'info' | 'warn' | 'error'; + +interface LogContext { + [key: string]: unknown; +} + +class Logger { + private isDevelopment = import.meta.env.DEV; + private logLevel: LogLevel = this.isDevelopment ? 'debug' : 'info'; + + private shouldLog(level: LogLevel): boolean { + const levels: LogLevel[] = ['debug', 'info', 'warn', 'error']; + const currentLevelIndex = levels.indexOf(this.logLevel); + const messageLevelIndex = levels.indexOf(level); + return messageLevelIndex >= currentLevelIndex; + } + + private formatMessage(level: LogLevel, message: string, context?: LogContext): string { + const timestamp = new Date().toISOString(); + const contextStr = context ? ` ${JSON.stringify(context)}` : ''; + return `[${timestamp}] [${level.toUpperCase()}] ${message}${contextStr}`; + } + + debug(message: string, context?: LogContext): void { + if (this.shouldLog('debug') && this.isDevelopment) { + console.debug(this.formatMessage('debug', message, context)); + } + } + + info(message: string, context?: LogContext): void { + if (this.shouldLog('info')) { + console.info(this.formatMessage('info', message, context)); + } + } + + warn(message: string, context?: LogContext): void { + if (this.shouldLog('warn')) { + console.warn(this.formatMessage('warn', message, context)); + } + } + + error(message: string, error?: Error | unknown, context?: LogContext): void { + if (this.shouldLog('error')) { + const errorContext = { + ...context, + error: error instanceof Error ? { + message: error.message, + stack: error.stack, + name: error.name, + } : String(error), + }; + console.error(this.formatMessage('error', message, errorContext)); + } + } + + /** + * Set log level + */ + setLevel(level: LogLevel): void { + this.logLevel = level; + } + + /** + * Get current log level + */ + getLevel(): LogLevel { + return this.logLevel; + } +} + +export const logger = new Logger(); +export default logger; + diff --git a/src/utils/sanitization.ts b/src/utils/sanitization.ts new file mode 100644 index 0000000..cd77eba --- /dev/null +++ b/src/utils/sanitization.ts @@ -0,0 +1,97 @@ +/** + * Input sanitization utilities + * Sanitizes user inputs to prevent XSS and other security issues + */ + +/** + * Sanitize string input by removing potentially dangerous characters + */ +export function sanitizeString(input: string): string { + if (typeof input !== 'string') { + return ''; + } + + // Remove null bytes + let sanitized = input.replace(/\0/g, ''); + + // Trim whitespace + sanitized = sanitized.trim(); + + // Remove control characters except newlines and tabs + sanitized = sanitized.replace(/[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]/g, ''); + + return sanitized; +} + +/** + * Sanitize search query + */ +export function sanitizeSearchQuery(query: string): string { + const sanitized = sanitizeString(query); + + // Remove excessive whitespace + return sanitized.replace(/\s+/g, ' '); +} + +/** + * Sanitize numeric input + */ +export function sanitizeNumber(input: string | number): number | null { + if (typeof input === 'number') { + return isNaN(input) ? null : input; + } + + if (typeof input !== 'string') { + return null; + } + + // Remove all non-numeric characters except decimal point and minus sign + const cleaned = input.replace(/[^\d.-]/g, ''); + const parsed = parseFloat(cleaned); + + return isNaN(parsed) ? null : parsed; +} + +/** + * Sanitize URL + */ +export function sanitizeUrl(url: string): string { + const sanitized = sanitizeString(url); + + // Basic URL validation - ensure it starts with http:// or https:// + if (sanitized && !sanitized.match(/^https?:\/\//i)) { + return ''; + } + + return sanitized; +} + +/** + * Sanitize HTML content (basic) + * For more advanced sanitization, consider using a library like DOMPurify + */ +export function sanitizeHtml(html: string): string { + const sanitized = sanitizeString(html); + + // Remove script tags and event handlers (basic protection) + return sanitized + .replace(/)<[^<]*)*<\/script>/gi, '') + .replace(/on\w+\s*=\s*["'][^"']*["']/gi, '') + .replace(/javascript:/gi, ''); +} + +/** + * Escape HTML special characters + */ +export function escapeHtml(text: string): string { + const map: Record = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + }; + + return text.replace(/[&<>"']/g, (m) => map[m]); +} + diff --git a/src/utils/validation.ts b/src/utils/validation.ts new file mode 100644 index 0000000..ce07dda --- /dev/null +++ b/src/utils/validation.ts @@ -0,0 +1,147 @@ +/** + * Input validation utilities + * Provides consistent validation for user inputs across the application + */ + +import { + MIN_SEARCH_QUERY_LENGTH, + MAX_SEARCH_QUERY_LENGTH, + MIN_MOVIE_ID, + MAX_MOVIE_ID, +} from '../constants'; + +/** + * Validate search query + */ +export function validateSearchQuery(query: string): { valid: boolean; error?: string } { + if (!query || typeof query !== 'string') { + return { valid: false, error: 'Search query is required' }; + } + + const trimmed = query.trim(); + + if (trimmed.length < MIN_SEARCH_QUERY_LENGTH) { + return { valid: false, error: `Search query must be at least ${MIN_SEARCH_QUERY_LENGTH} character(s)` }; + } + + if (trimmed.length > MAX_SEARCH_QUERY_LENGTH) { + return { valid: false, error: `Search query must be less than ${MAX_SEARCH_QUERY_LENGTH} characters` }; + } + + return { valid: true }; +} + +/** + * Validate movie ID + */ +export function validateMovieId(id: number | string): { valid: boolean; error?: string; parsedId?: number } { + if (id === null || id === undefined) { + return { valid: false, error: 'Movie ID is required' }; + } + + const parsedId = typeof id === 'string' ? parseInt(id, 10) : id; + + if (isNaN(parsedId)) { + return { valid: false, error: 'Movie ID must be a valid number' }; + } + + if (parsedId < MIN_MOVIE_ID || parsedId > MAX_MOVIE_ID) { + return { valid: false, error: `Movie ID must be between ${MIN_MOVIE_ID} and ${MAX_MOVIE_ID}` }; + } + + return { valid: true, parsedId }; +} + +/** + * Validate URL + */ +export function validateUrl(url: string): { valid: boolean; error?: string } { + if (!url || typeof url !== 'string') { + return { valid: false, error: 'URL is required' }; + } + + try { + new URL(url); + return { valid: true }; + } catch { + return { valid: false, error: 'Invalid URL format' }; + } +} + +/** + * Validate numeric input + */ +export function validateNumber( + value: number | string, + options: { min?: number; max?: number; required?: boolean } = {} +): { valid: boolean; error?: string; parsedValue?: number } { + const { min, max, required = true } = options; + + if (value === null || value === undefined || value === '') { + if (required) { + return { valid: false, error: 'Value is required' }; + } + return { valid: true, parsedValue: undefined }; + } + + const parsedValue = typeof value === 'string' ? parseFloat(value) : value; + + if (isNaN(parsedValue)) { + return { valid: false, error: 'Value must be a valid number' }; + } + + if (min !== undefined && parsedValue < min) { + return { valid: false, error: `Value must be at least ${min}` }; + } + + if (max !== undefined && parsedValue > max) { + return { valid: false, error: `Value must be at most ${max}` }; + } + + return { valid: true, parsedValue }; +} + +/** + * Validate quality string + */ +export function validateQuality(quality: string): { valid: boolean; error?: string } { + const validQualities = ['480p', '720p', '1080p', '1080p.x265', '2160p', '3D']; + + if (!quality || typeof quality !== 'string') { + return { valid: false, error: 'Quality is required' }; + } + + if (!validQualities.includes(quality)) { + return { valid: false, error: `Quality must be one of: ${validQualities.join(', ')}` }; + } + + return { valid: true }; +} + +/** + * Validate genre string + */ +export function validateGenre(genre: string): { valid: boolean; error?: string } { + if (!genre || typeof genre !== 'string') { + return { valid: false, error: 'Genre is required' }; + } + + // Genre should be a non-empty string + if (genre.trim().length === 0) { + return { valid: false, error: 'Genre cannot be empty' }; + } + + return { valid: true }; +} + +/** + * Sanitize search query - remove special characters and normalize + */ +export function sanitizeSearchQuery(query: string): string { + if (!query || typeof query !== 'string') { + return ''; + } + + return query.trim().replace(/[^\w\s-]/g, ''); +} + diff --git a/vite.config.ts b/vite.config.ts index f20fcca..cb10561 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -30,7 +30,7 @@ export default defineConfig({ proxyTimeout: 30000, }, '/api/eztv': { - target: 'https://eztv.tf/api', + target: 'https://eztvx.to/api', changeOrigin: true, rewrite: (path) => path.replace(/^\/api\/eztv/, ''), secure: false, @@ -41,17 +41,25 @@ export default defineConfig({ target: 'https://api.themoviedb.org/3', changeOrigin: true, rewrite: (path) => path.replace(/^\/api\/tmdb/, ''), - secure: false, - timeout: 30000, - proxyTimeout: 30000, - }, - '/api/rarbg': { - target: 'https://torrentapi.org', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/api\/rarbg/, '/pubapi_v2.php'), - secure: false, + secure: true, timeout: 30000, proxyTimeout: 30000, + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + 'Accept': 'application/json', + 'Accept-Language': 'en-US,en;q=0.9', + }, + configure: (proxy, _options) => { + proxy.on('error', (err, req, res) => { + console.log('TMDB proxy error for:', req.url, err.message); + // Don't try to respond here, let it fall through to retry logic + }); + proxy.on('proxyReq', (proxyReq, req, res) => { + proxyReq.setTimeout(30000); + // Keep host header but remove referer + proxyReq.removeHeader('referer'); + }); + }, }, }, },