diff --git a/.gitignore b/.gitignore index ef726f2..1d928f0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ node_modules/ coverage .env .eslintcache - +/lib-obf +.DS_Store diff --git a/package.json b/package.json index ccc0215..1fafa9a 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,9 @@ "node-fetch": "^3.3.2", "prettier": "^3.6.2", "puppeteer": "^22.15.0", + "rollup-plugin-obfuscator": "^1.1.0", "spinnies": "^0.5.1", + "terser": "^5.44.0", "tsc-alias": "^1.8.16", "tsconfig-paths": "^4.2.0", "typescript": "^5.9.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 664a8c7..744f829 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,7 +50,7 @@ importers: devDependencies: '@nabla/vite-plugin-eslint': specifier: ^2.0.6 - version: 2.0.6(eslint@8.57.1)(vite@5.4.21(@types/node@24.9.1)) + version: 2.0.6(eslint@8.57.1)(vite@5.4.21(@types/node@24.9.1)(terser@5.44.0)) '@types/cookie': specifier: ^0.6.0 version: 0.6.0 @@ -77,7 +77,7 @@ importers: version: 7.18.0(eslint@8.57.1)(typescript@5.9.3) '@vitest/coverage-v8': specifier: ^1.6.1 - version: 1.6.1(vitest@1.6.1(@types/node@24.9.1)) + version: 1.6.1(vitest@1.6.1(@types/node@24.9.1)(jsdom@27.1.0(canvas@3.2.0))(terser@5.44.0)) commander: specifier: ^12.1.0 version: 12.1.0 @@ -114,9 +114,15 @@ importers: puppeteer: specifier: ^22.15.0 version: 22.15.0(typescript@5.9.3) + rollup-plugin-obfuscator: + specifier: ^1.1.0 + version: 1.1.0(javascript-obfuscator@4.1.1)(rollup@4.52.5) spinnies: specifier: ^0.5.1 version: 0.5.1 + terser: + specifier: ^5.44.0 + version: 5.44.0 tsc-alias: specifier: ^1.8.16 version: 1.8.16 @@ -128,23 +134,35 @@ importers: version: 5.9.3 vite: specifier: ^5.4.21 - version: 5.4.21(@types/node@24.9.1) + version: 5.4.21(@types/node@24.9.1)(terser@5.44.0) vite-node: specifier: ^1.6.1 - version: 1.6.1(@types/node@24.9.1) + version: 1.6.1(@types/node@24.9.1)(terser@5.44.0) vite-plugin-dts: specifier: ^3.9.1 - version: 3.9.1(@types/node@24.9.1)(rollup@4.52.5)(typescript@5.9.3)(vite@5.4.21(@types/node@24.9.1)) + version: 3.9.1(@types/node@24.9.1)(rollup@4.52.5)(typescript@5.9.3)(vite@5.4.21(@types/node@24.9.1)(terser@5.44.0)) vitest: specifier: ^1.6.1 - version: 1.6.1(@types/node@24.9.1) + version: 1.6.1(@types/node@24.9.1)(jsdom@27.1.0(canvas@3.2.0))(terser@5.44.0) packages: + '@acemir/cssom@0.9.19': + resolution: {integrity: sha512-Pp2gAQXPZ2o7lt4j0IMwNRXqQ3pagxtDj5wctL5U2Lz4oV0ocDNlkgx4DpxfyKav4S/bePuI+SMqcBSUHLy9kg==} + '@ampproject/remapping@2.3.0': resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} + '@asamuzakjp/css-color@4.0.5': + resolution: {integrity: sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==} + + '@asamuzakjp/dom-selector@6.7.4': + resolution: {integrity: sha512-buQDjkm+wDPXd6c13534URWZqbz0RP5PAhXZ+LIoa5LgwInT9HVJvGIJivg75vi8I13CxDGdTnz+aY5YUJlIAA==} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -169,6 +187,38 @@ packages: '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-syntax-patches-for-csstree@1.0.15': + resolution: {integrity: sha512-q0p6zkVq2lJnmzZVPR33doA51G7YOja+FBvRdp5ISIthL0MtFCgYHHhR563z9WFGxcOn0WfjSkPDJ5Qig3H3Sw==} + engines: {node: '>=18'} + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + '@emnapi/core@1.6.0': resolution: {integrity: sha512-zq/ay+9fNIJJtJiZxdTnXS20PllcYMX3OE23ESc4HK/bdYu3cOWYVhsOhVnXALfU/uqJIxn5NBPd9z4v+SfoSg==} @@ -351,6 +401,14 @@ packages: resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} + '@javascript-obfuscator/escodegen@2.3.0': + resolution: {integrity: sha512-QVXwMIKqYMl3KwtTirYIA6gOCiJ0ZDtptXqAv/8KWLG9uQU2fZqTVy7a/A5RvcoZhbDoFfveTxuGxJ5ibzQtkw==} + engines: {node: '>=6.0'} + + '@javascript-obfuscator/estraverse@5.4.0': + resolution: {integrity: sha512-CZFX7UZVN9VopGbjTx4UXaXsi9ewoM1buL0kY7j1ftYdSs7p2spv9opxFjHlQ/QGTgh4UqufYqJJ0WKLml7b6w==} + engines: {node: '>=4.0'} + '@jest/schemas@29.6.3': resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -362,6 +420,9 @@ packages: resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} @@ -589,6 +650,9 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/minimatch@3.0.5': + resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==} + '@types/node-fetch@2.6.13': resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==} @@ -604,6 +668,9 @@ packages: '@types/spinnies@0.5.3': resolution: {integrity: sha512-HYrOubG2TVgRQRKcW1HJ/1eJIIBpLqDoJo551McJgWdO8xzxnaxu/bPKdqC/7okoEy4ZZjy3I4/DwK1sz2OCog==} + '@types/validator@13.15.3': + resolution: {integrity: sha512-7bcUmDyS6PN3EuD9SlGGOxM77F8WLVsrwkxyWxKnxzmXoequ6c7741QBrANq6htVRGOITJ7z72mTP6Z4XyuG+Q==} + '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} @@ -827,6 +894,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.8.2: + resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==} + engines: {node: '>=0.4.0'} + hasBin: true + agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} @@ -872,6 +944,10 @@ packages: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} + array-differ@3.0.0: + resolution: {integrity: sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==} + engines: {node: '>=8'} + array-includes@3.1.9: resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} engines: {node: '>= 0.4'} @@ -896,6 +972,13 @@ packages: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} + arrify@2.0.1: + resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==} + engines: {node: '>=8'} + + assert@2.0.0: + resolution: {integrity: sha512-se5Cd+js9dXJnu6Ag2JFc00t+HmHOen+8Q+L7O9zI0PqQXr20uk2J0XQqMxZEeo5U50o8Nvmmx7dZrl+Ufr35A==} + assertion-error@1.1.0: resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} @@ -970,10 +1053,16 @@ packages: resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==} engines: {node: '>=10.0.0'} + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -990,6 +1079,9 @@ packages: buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} @@ -1013,6 +1105,10 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} + canvas@3.2.0: + resolution: {integrity: sha512-jk0GxrLtUEmW/TmFsk2WghvgHe8B0pxGilqCL21y8lHkPUGa6FTsnCNtHPOzT8O3y+N+m3espawV80bbBlgfTA==} + engines: {node: ^18.12.0 || >= 20.9.0} + chai@4.5.0: resolution: {integrity: sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==} engines: {node: '>=4'} @@ -1025,6 +1121,16 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + chance@1.1.9: + resolution: {integrity: sha512-TfxnA/DcZXRTA4OekA2zL9GH8qscbbl6X0ZqU4tXhGveVY/mXWvEQLt5GwZcYXTEyEFflVtj+pG8nc8EwSm1RQ==} + + char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + + charenc@0.0.2: + resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} + check-error@1.0.3: resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} @@ -1039,11 +1145,17 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + chromium-bidi@0.6.3: resolution: {integrity: sha512-qXlsCmpCZJAnoTYI83Iu6EdYQpMYdVkCfq08KDh2pmlVqK5t5IA9mGs4/LwCwp4fqisSOMXZxP3HIh8w8aRn0A==} peerDependencies: devtools-protocol: '*' + class-validator@0.14.1: + resolution: {integrity: sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==} + cli-cursor@3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} @@ -1069,10 +1181,17 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + commander@10.0.0: + resolution: {integrity: sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA==} + engines: {node: '>=14'} + commander@12.1.0: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + commander@9.5.0: resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} engines: {node: ^12.20.0 || >=14} @@ -1111,16 +1230,27 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + crypt@0.0.2: + resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} + crypto-js@4.2.0: resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} css-select@5.2.2: resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + css-tree@3.1.0: + resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + css-what@6.2.2: resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} engines: {node: '>= 6'} + cssstyle@5.3.2: + resolution: {integrity: sha512-zDMqXh8Vs1CdRYZQ2M633m/SFgcjlu8RB8b/1h82i+6vpArF507NSYIWJHGlJaTWoS+imcnctmEz43txhbVkOw==} + engines: {node: '>=20'} + data-uri-to-buffer@4.0.1: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} @@ -1129,6 +1259,10 @@ packages: resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} engines: {node: '>= 14'} + data-urls@6.0.0: + resolution: {integrity: sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==} + engines: {node: '>=20'} + data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} @@ -1161,10 +1295,21 @@ packages: supports-color: optional: true + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + deep-eql@4.1.4: resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==} engines: {node: '>=6'} + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -1184,6 +1329,10 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + devtools-protocol@0.0.1312386: resolution: {integrity: sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA==} @@ -1277,6 +1426,9 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} + es6-object-assign@1.1.0: + resolution: {integrity: sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==} + esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} @@ -1373,10 +1525,18 @@ packages: eslint-config-prettier: optional: true + eslint-scope@7.1.1: + resolution: {integrity: sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + eslint-scope@7.2.2: resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + eslint-visitor-keys@3.3.0: + resolution: {integrity: sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1429,6 +1589,10 @@ packages: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + extract-zip@2.0.1: resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} engines: {node: '>= 10.17.0'} @@ -1503,6 +1667,9 @@ packages: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -1567,6 +1734,9 @@ packages: resolution: {integrity: sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==} engines: {node: '>= 14'} + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -1639,6 +1809,10 @@ packages: hls-parser@0.13.6: resolution: {integrity: sha512-I40sl22E2muqeSTpG8kMN2dAegAhubkXPXtnsUXFwdKwZK47d1Q+XwuX32VMZ++AZU5oeQIZqAnGNHxSG1sWaw==} + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -1657,6 +1831,10 @@ packages: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -1683,14 +1861,24 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + inversify@6.0.1: + resolution: {integrity: sha512-B3ex30927698TJENHR++8FfEaJGqoWOgI6ZY5Ht/nLUsFCwHn6akbwtnUAPCgUepAnTpe2qHxhDNjoKLyz6rgQ==} + ip-address@10.0.1: resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==} engines: {node: '>= 12'} + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -1714,6 +1902,9 @@ packages: resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} engines: {node: '>= 0.4'} + is-buffer@1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + is-bun-module@2.0.0: resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} @@ -1757,6 +1948,10 @@ packages: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} + is-nan@1.3.2: + resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==} + engines: {node: '>= 0.4'} + is-negative-zero@2.0.3: resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} engines: {node: '>= 0.4'} @@ -1773,6 +1968,9 @@ packages: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -1839,9 +2037,18 @@ packages: resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} engines: {node: '>=8'} + javascript-obfuscator@4.1.1: + resolution: {integrity: sha512-gt+KZpIIrrxXHEQGD8xZrL8mTRwRY0U76/xz/YX0gZdPrSqQhT/c7dYLASlLlecT3r+FxE7je/+C0oLnTDCx4A==} + engines: {node: '>=12.22.0'} + hasBin: true + jju@1.4.0: resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} + js-string-escape@1.0.1: + resolution: {integrity: sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==} + engines: {node: '>= 0.8'} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1852,6 +2059,15 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true + jsdom@27.1.0: + resolution: {integrity: sha512-Pcfm3eZ+eO4JdZCXthW9tCDT3nF4K+9dmeZ+5X39n+Kqz0DDIABRP5CAEOHRFZk8RGuC2efksTJxrjp8EXCunQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -1882,10 +2098,17 @@ packages: kolorist@1.8.0: resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + levn@0.3.0: + resolution: {integrity: sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==} + engines: {node: '>= 0.8.0'} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + libphonenumber-js@1.12.25: + resolution: {integrity: sha512-u90tUu/SEF8b+RaDKCoW7ZNFDakyBtFlX1ex3J+VH+ElWes/UaitJLt/w4jGu8uAE41lltV/s+kMVtywcMEg7g==} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -1914,6 +2137,10 @@ packages: loupe@2.3.7: resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + lru-cache@11.2.2: + resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==} + engines: {node: 20 || >=22} + lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} @@ -1936,6 +2163,12 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + md5@2.3.0: + resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} + + mdn-data@2.12.2: + resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -1963,6 +2196,10 @@ packages: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + minimatch@3.0.8: resolution: {integrity: sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==} @@ -1979,6 +2216,14 @@ packages: mitt@3.0.1: resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + mkdirp@2.1.3: + resolution: {integrity: sha512-sjAkg21peAG9HS+Dkx7hlG9Ztx7HLeKnvB3NQRcu/mltCVmvkF0pisbiTSfDVYTT86XEfZrTUosLdZLStquZUw==} + engines: {node: '>=10'} + hasBin: true + mlly@1.8.0: resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} @@ -1988,6 +2233,10 @@ packages: muggle-string@0.3.1: resolution: {integrity: sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==} + multimatch@5.0.0: + resolution: {integrity: sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==} + engines: {node: '>=10'} + mylas@2.1.13: resolution: {integrity: sha512-+MrqnJRtxdF+xngFfUUkIMQrUUL0KsxbADUkn23Z/4ibGg192Q+z+CQyiYwvWTsYjJygmMR8+w3ZDa98Zh6ESg==} engines: {node: '>=12.0.0'} @@ -1997,6 +2246,9 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + napi-postinstall@0.3.4: resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -2009,6 +2261,13 @@ packages: resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} engines: {node: '>= 0.4.0'} + node-abi@3.80.0: + resolution: {integrity: sha512-LyPuZJcI9HVwzXK1GPxWNzrr+vr8Hp/3UqlmWxxh8p54U1ZbclOqbSog9lWHaCX+dBaiGi6n/hIX+mKu74GmPA==} + engines: {node: '>=10'} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} @@ -2033,6 +2292,10 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} + object-is@1.1.6: + resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} + engines: {node: '>= 0.4'} + object-keys@1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} @@ -2068,6 +2331,14 @@ packages: resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} engines: {node: '>=12'} + opencollective-postinstall@2.0.3: + resolution: {integrity: sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==} + hasBin: true + + optionator@0.8.3: + resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==} + engines: {node: '>= 0.8.0'} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -2110,6 +2381,9 @@ packages: parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + parse5@8.0.0: + resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -2174,6 +2448,15 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + hasBin: true + + prelude-ls@1.1.2: + resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} + engines: {node: '>= 0.8.0'} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -2191,6 +2474,10 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + progress@2.0.3: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} @@ -2226,13 +2513,24 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + reflect-metadata@0.1.13: + resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==} + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -2245,6 +2543,10 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -2273,6 +2575,12 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true + rollup-plugin-obfuscator@1.1.0: + resolution: {integrity: sha512-cMfQIKyGePlfHGGO+rSDhSATMBx7WWxXW/X66c53HylUE/owanTbG6nhttUQOkbdCiQH8tClskNEH/IPRoqZwA==} + peerDependencies: + javascript-obfuscator: '*' + rollup: ^2.56.3||^3.0.0||^4.0.0 + rollup@4.52.5: resolution: {integrity: sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -2285,6 +2593,9 @@ packages: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-push-apply@1.0.0: resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} engines: {node: '>= 0.4'} @@ -2293,6 +2604,13 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -2356,6 +2674,12 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -2376,6 +2700,9 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} @@ -2406,6 +2733,9 @@ packages: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} + string-template@1.0.0: + resolution: {integrity: sha512-SLqR3GBUXuoPP5MmYtD7ompvXiG87QjT6lzOszyXjTM86Uu7At7vNnt2xgyTLq5o9T4IxTYFyGxcULqpsmsfdg==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -2422,6 +2752,12 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + stringz@2.1.0: + resolution: {integrity: sha512-KlywLT+MZ+v0IRepfMxRtnSvDCMc3nR1qqCs3m/qIbSOWkNZYT8XHQA31rS3TnKp0c5xjZu3M4GY/2aRKSi/6A==} + strip-ansi@5.2.0: resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==} engines: {node: '>=6'} @@ -2438,6 +2774,10 @@ packages: resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} engines: {node: '>=12'} + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -2461,16 +2801,31 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + synckit@0.11.11: resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} engines: {node: ^14.18.0 || >=16.0.0} + tar-fs@2.1.4: + resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} + tar-fs@3.1.1: resolution: {integrity: sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==} + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + tar-stream@3.1.7: resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} + terser@5.44.0: + resolution: {integrity: sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==} + engines: {node: '>=10'} + hasBin: true + test-exclude@6.0.0: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} @@ -2499,10 +2854,25 @@ packages: resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} engines: {node: '>=14.0.0'} + tldts-core@7.0.17: + resolution: {integrity: sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==} + + tldts@7.0.17: + resolution: {integrity: sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ==} + hasBin: true + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + tough-cookie@6.0.0: + resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} + engines: {node: '>=16'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + ts-api-utils@1.4.3: resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} engines: {node: '>=16'} @@ -2521,9 +2891,19 @@ packages: resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} engines: {node: '>=6'} + tslib@2.5.0: + resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + type-check@0.3.2: + resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==} + engines: {node: '>= 0.8.0'} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -2591,6 +2971,12 @@ packages: urlpattern-polyfill@10.0.0: resolution: {integrity: sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==} + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + validator@13.15.15: resolution: {integrity: sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==} engines: {node: '>= 0.10'} @@ -2675,10 +3061,30 @@ packages: peerDependencies: typescript: '*' + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + web-streams-polyfill@3.3.3: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} + webidl-conversions@8.0.0: + resolution: {integrity: sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==} + engines: {node: '>=20'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@15.1.0: + resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==} + engines: {node: '>=20'} + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -2728,6 +3134,13 @@ packages: utf-8-validate: optional: true + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -2764,11 +3177,35 @@ packages: snapshots: + '@acemir/cssom@0.9.19': + optional: true + '@ampproject/remapping@2.3.0': dependencies: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 + '@asamuzakjp/css-color@4.0.5': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 11.2.2 + optional: true + + '@asamuzakjp/dom-selector@6.7.4': + dependencies: + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.1.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.2.2 + optional: true + + '@asamuzakjp/nwsapi@2.3.9': + optional: true + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -2790,6 +3227,34 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} + '@csstools/color-helpers@5.1.0': + optional: true + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + optional: true + + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + optional: true + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + optional: true + + '@csstools/css-syntax-patches-for-csstree@1.0.15': + optional: true + + '@csstools/css-tokenizer@3.0.4': + optional: true + '@emnapi/core@1.6.0': dependencies: '@emnapi/wasi-threads': 1.1.0 @@ -2912,6 +3377,17 @@ snapshots: '@istanbuljs/schema@0.1.3': {} + '@javascript-obfuscator/escodegen@2.3.0': + dependencies: + '@javascript-obfuscator/estraverse': 5.4.0 + esprima: 4.0.1 + esutils: 2.0.3 + optionator: 0.8.3 + optionalDependencies: + source-map: 0.6.1 + + '@javascript-obfuscator/estraverse@5.4.0': {} + '@jest/schemas@29.6.3': dependencies: '@sinclair/typebox': 0.27.8 @@ -2923,6 +3399,11 @@ snapshots: '@jridgewell/resolve-uri@3.1.2': {} + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/sourcemap-codec@1.5.5': {} '@jridgewell/trace-mapping@0.3.31': @@ -2965,13 +3446,13 @@ snapshots: '@microsoft/tsdoc@0.14.2': {} - '@nabla/vite-plugin-eslint@2.0.6(eslint@8.57.1)(vite@5.4.21(@types/node@24.9.1))': + '@nabla/vite-plugin-eslint@2.0.6(eslint@8.57.1)(vite@5.4.21(@types/node@24.9.1)(terser@5.44.0))': dependencies: '@types/eslint': 9.6.1 chalk: 4.1.2 debug: 4.4.3 eslint: 8.57.1 - vite: 5.4.21(@types/node@24.9.1) + vite: 5.4.21(@types/node@24.9.1)(terser@5.44.0) transitivePeerDependencies: - supports-color @@ -3148,6 +3629,8 @@ snapshots: '@types/json5@0.0.29': {} + '@types/minimatch@3.0.5': {} + '@types/node-fetch@2.6.13': dependencies: '@types/node': 24.9.1 @@ -3167,6 +3650,8 @@ snapshots: '@types/spinnies@0.5.3': {} + '@types/validator@13.15.3': {} + '@types/yauzl@2.10.3': dependencies: '@types/node': 24.9.1 @@ -3314,7 +3799,7 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vitest/coverage-v8@1.6.1(vitest@1.6.1(@types/node@24.9.1))': + '@vitest/coverage-v8@1.6.1(vitest@1.6.1(@types/node@24.9.1)(jsdom@27.1.0(canvas@3.2.0))(terser@5.44.0))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 0.2.3 @@ -3329,7 +3814,7 @@ snapshots: std-env: 3.10.0 strip-literal: 2.1.1 test-exclude: 6.0.0 - vitest: 1.6.1(@types/node@24.9.1) + vitest: 1.6.1(@types/node@24.9.1)(jsdom@27.1.0(canvas@3.2.0))(terser@5.44.0) transitivePeerDependencies: - supports-color @@ -3418,6 +3903,8 @@ snapshots: acorn@8.15.0: {} + acorn@8.8.2: {} + agent-base@7.1.4: {} ajv@6.12.6: @@ -3459,6 +3946,8 @@ snapshots: call-bound: 1.0.4 is-array-buffer: 3.0.5 + array-differ@3.0.0: {} + array-includes@3.1.9: dependencies: call-bind: 1.0.8 @@ -3506,6 +3995,15 @@ snapshots: get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 + arrify@2.0.1: {} + + assert@2.0.0: + dependencies: + es6-object-assign: 1.1.0 + is-nan: 1.3.2 + object-is: 1.1.6 + util: 0.12.5 + assertion-error@1.1.0: {} ast-types@0.13.4: @@ -3565,8 +4063,20 @@ snapshots: basic-ftp@5.0.5: {} + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + optional: true + binary-extensions@2.3.0: {} + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + optional: true + boolbase@1.0.0: {} brace-expansion@1.1.12: @@ -3584,6 +4094,8 @@ snapshots: buffer-crc32@0.2.13: {} + buffer-from@1.1.2: {} + buffer@5.7.1: dependencies: base64-js: 1.5.1 @@ -3610,6 +4122,12 @@ snapshots: callsites@3.1.0: {} + canvas@3.2.0: + dependencies: + node-addon-api: 7.1.1 + prebuild-install: 7.1.3 + optional: true + chai@4.5.0: dependencies: assertion-error: 1.1.0 @@ -3631,6 +4149,12 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + chance@1.1.9: {} + + char-regex@1.0.2: {} + + charenc@0.0.2: {} + check-error@1.0.3: dependencies: get-func-name: 2.0.2 @@ -3666,6 +4190,9 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + chownr@1.1.4: + optional: true + chromium-bidi@0.6.3(devtools-protocol@0.0.1312386): dependencies: devtools-protocol: 0.0.1312386 @@ -3673,6 +4200,12 @@ snapshots: urlpattern-polyfill: 10.0.0 zod: 3.23.8 + class-validator@0.14.1: + dependencies: + '@types/validator': 13.15.3 + libphonenumber-js: 1.12.25 + validator: 13.15.15 + cli-cursor@3.1.0: dependencies: restore-cursor: 3.1.0 @@ -3699,8 +4232,12 @@ snapshots: dependencies: delayed-stream: 1.0.0 + commander@10.0.0: {} + commander@12.1.0: {} + commander@2.20.3: {} + commander@9.5.0: {} computeds@0.0.1: {} @@ -3732,6 +4269,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + crypt@0.0.2: {} + crypto-js@4.2.0: {} css-select@5.2.2: @@ -3742,12 +4281,31 @@ snapshots: domutils: 3.2.2 nth-check: 2.1.1 + css-tree@3.1.0: + dependencies: + mdn-data: 2.12.2 + source-map-js: 1.2.1 + optional: true + css-what@6.2.2: {} + cssstyle@5.3.2: + dependencies: + '@asamuzakjp/css-color': 4.0.5 + '@csstools/css-syntax-patches-for-csstree': 1.0.15 + css-tree: 3.1.0 + optional: true + data-uri-to-buffer@4.0.1: {} data-uri-to-buffer@6.0.2: {} + data-urls@6.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 15.1.0 + optional: true + data-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 @@ -3776,10 +4334,21 @@ snapshots: dependencies: ms: 2.1.3 + decimal.js@10.6.0: + optional: true + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + optional: true + deep-eql@4.1.4: dependencies: type-detect: 4.1.0 + deep-extend@0.6.0: + optional: true + deep-is@0.1.4: {} define-data-property@1.1.4: @@ -3802,6 +4371,9 @@ snapshots: delayed-stream@1.0.0: {} + detect-libc@2.1.2: + optional: true + devtools-protocol@0.0.1312386: {} diff-sequences@29.6.3: {} @@ -3947,6 +4519,8 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 + es6-object-assign@1.1.0: {} + esbuild@0.21.5: optionalDependencies: '@esbuild/aix-ppc64': 0.21.5 @@ -4073,11 +4647,18 @@ snapshots: '@types/eslint': 9.6.1 eslint-config-prettier: 9.1.2(eslint@8.57.1) + eslint-scope@7.1.1: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + eslint-scope@7.2.2: dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 + eslint-visitor-keys@3.3.0: {} + eslint-visitor-keys@3.4.3: {} eslint@8.57.1: @@ -4169,6 +4750,9 @@ snapshots: signal-exit: 4.1.0 strip-final-newline: 3.0.0 + expand-template@2.0.3: + optional: true + extract-zip@2.0.1: dependencies: debug: 4.4.3 @@ -4251,6 +4835,9 @@ snapshots: dependencies: fetch-blob: 3.2.0 + fs-constants@1.0.0: + optional: true + fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11 @@ -4325,6 +4912,9 @@ snapshots: transitivePeerDependencies: - supports-color + github-from-package@0.0.0: + optional: true + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -4394,6 +4984,11 @@ snapshots: hls-parser@0.13.6: {} + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + optional: true + html-escaper@2.0.2: {} htmlparser2@8.0.2: @@ -4419,6 +5014,11 @@ snapshots: human-signals@5.0.0: {} + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + optional: true + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -4439,14 +5039,24 @@ snapshots: inherits@2.0.4: {} + ini@1.3.8: + optional: true + internal-slot@1.1.0: dependencies: es-errors: 1.3.0 hasown: 2.0.2 side-channel: 1.1.0 + inversify@6.0.1: {} + ip-address@10.0.1: {} + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 @@ -4476,6 +5086,8 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 + is-buffer@1.1.6: {} + is-bun-module@2.0.0: dependencies: semver: 7.7.3 @@ -4519,6 +5131,11 @@ snapshots: is-map@2.0.3: {} + is-nan@1.3.2: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + is-negative-zero@2.0.3: {} is-number-object@1.1.1: @@ -4530,6 +5147,9 @@ snapshots: is-path-inside@3.0.3: {} + is-potential-custom-element-name@1.0.1: + optional: true + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -4598,8 +5218,36 @@ snapshots: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 + javascript-obfuscator@4.1.1: + dependencies: + '@javascript-obfuscator/escodegen': 2.3.0 + '@javascript-obfuscator/estraverse': 5.4.0 + acorn: 8.8.2 + assert: 2.0.0 + chalk: 4.1.2 + chance: 1.1.9 + class-validator: 0.14.1 + commander: 10.0.0 + eslint-scope: 7.1.1 + eslint-visitor-keys: 3.3.0 + fast-deep-equal: 3.1.3 + inversify: 6.0.1 + js-string-escape: 1.0.1 + md5: 2.3.0 + mkdirp: 2.1.3 + multimatch: 5.0.0 + opencollective-postinstall: 2.0.3 + process: 0.11.10 + reflect-metadata: 0.1.13 + source-map-support: 0.5.21 + string-template: 1.0.0 + stringz: 2.1.0 + tslib: 2.5.0 + jju@1.4.0: {} + js-string-escape@1.0.1: {} + js-tokens@4.0.0: {} js-tokens@9.0.1: {} @@ -4608,6 +5256,36 @@ snapshots: dependencies: argparse: 2.0.1 + jsdom@27.1.0(canvas@3.2.0): + dependencies: + '@acemir/cssom': 0.9.19 + '@asamuzakjp/dom-selector': 6.7.4 + cssstyle: 5.3.2 + data-urls: 6.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + parse5: 8.0.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 15.1.0 + ws: 8.18.3 + xml-name-validator: 5.0.0 + optionalDependencies: + canvas: 3.2.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + optional: true + json-buffer@3.0.1: {} json-parse-even-better-errors@2.3.1: {} @@ -4632,11 +5310,18 @@ snapshots: kolorist@1.8.0: {} + levn@0.3.0: + dependencies: + prelude-ls: 1.1.2 + type-check: 0.3.2 + levn@0.4.1: dependencies: prelude-ls: 1.2.1 type-check: 0.4.0 + libphonenumber-js@1.12.25: {} + lines-and-columns@1.2.4: {} local-pkg@0.5.1: @@ -4660,6 +5345,9 @@ snapshots: dependencies: get-func-name: 2.0.2 + lru-cache@11.2.2: + optional: true + lru-cache@6.0.0: dependencies: yallist: 4.0.0 @@ -4682,6 +5370,15 @@ snapshots: math-intrinsics@1.1.0: {} + md5@2.3.0: + dependencies: + charenc: 0.0.2 + crypt: 0.0.2 + is-buffer: 1.1.6 + + mdn-data@2.12.2: + optional: true + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -4701,6 +5398,9 @@ snapshots: mimic-fn@4.0.0: {} + mimic-response@3.1.0: + optional: true + minimatch@3.0.8: dependencies: brace-expansion: 1.1.12 @@ -4717,6 +5417,11 @@ snapshots: mitt@3.0.1: {} + mkdirp-classic@0.5.3: + optional: true + + mkdirp@2.1.3: {} + mlly@1.8.0: dependencies: acorn: 8.15.0 @@ -4728,16 +5433,35 @@ snapshots: muggle-string@0.3.1: {} + multimatch@5.0.0: + dependencies: + '@types/minimatch': 3.0.5 + array-differ: 3.0.0 + array-union: 2.1.0 + arrify: 2.0.1 + minimatch: 3.1.2 + mylas@2.1.13: {} nanoid@3.3.11: {} + napi-build-utils@2.0.0: + optional: true + napi-postinstall@0.3.4: {} natural-compare@1.4.0: {} netmask@2.0.2: {} + node-abi@3.80.0: + dependencies: + semver: 7.7.3 + optional: true + + node-addon-api@7.1.1: + optional: true + node-domexception@1.0.0: {} node-fetch@3.3.2: @@ -4758,6 +5482,11 @@ snapshots: object-inspect@1.13.4: {} + object-is@1.1.6: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + object-keys@1.1.1: {} object.assign@4.1.7: @@ -4808,6 +5537,17 @@ snapshots: dependencies: mimic-fn: 4.0.0 + opencollective-postinstall@2.0.3: {} + + optionator@0.8.3: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.3.0 + prelude-ls: 1.1.2 + type-check: 0.3.2 + word-wrap: 1.2.5 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -4873,6 +5613,11 @@ snapshots: dependencies: entities: 6.0.1 + parse5@8.0.0: + dependencies: + entities: 6.0.1 + optional: true + path-browserify@1.0.1: {} path-exists@4.0.0: {} @@ -4919,6 +5664,24 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.1.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.80.0 + pump: 3.0.3 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.4 + tunnel-agent: 0.6.0 + optional: true + + prelude-ls@1.1.2: {} + prelude-ls@1.2.1: {} prettier-linter-helpers@1.0.0: @@ -4933,6 +5696,8 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 + process@0.11.10: {} + progress@2.0.3: {} proxy-agent@6.5.0: @@ -4991,12 +5756,29 @@ snapshots: queue-microtask@1.2.3: {} + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + optional: true + react-is@18.3.1: {} + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + optional: true + readdirp@3.6.0: dependencies: picomatch: 2.3.1 + reflect-metadata@0.1.13: {} + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -5019,6 +5801,9 @@ snapshots: require-directory@2.1.1: {} + require-from-string@2.0.2: + optional: true + resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -5045,6 +5830,12 @@ snapshots: dependencies: glob: 7.2.3 + rollup-plugin-obfuscator@1.1.0(javascript-obfuscator@4.1.1)(rollup@4.52.5): + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@4.52.5) + javascript-obfuscator: 4.1.1 + rollup: 4.52.5 + rollup@4.52.5: dependencies: '@types/estree': 1.0.8 @@ -5085,6 +5876,9 @@ snapshots: has-symbols: 1.1.0 isarray: 2.0.5 + safe-buffer@5.2.1: + optional: true + safe-push-apply@1.0.0: dependencies: es-errors: 1.3.0 @@ -5096,6 +5890,14 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 + safer-buffer@2.1.2: + optional: true + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + optional: true + semver@6.3.1: {} semver@7.5.4: @@ -5168,6 +5970,16 @@ snapshots: signal-exit@4.1.0: {} + simple-concat@1.0.1: + optional: true + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + optional: true + slash@3.0.0: {} smart-buffer@4.2.0: {} @@ -5187,6 +5999,11 @@ snapshots: source-map-js@1.2.1: {} + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + source-map@0.6.1: {} spinnies@0.5.1: @@ -5219,6 +6036,8 @@ snapshots: string-argv@0.3.2: {} + string-template@1.0.0: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -5248,6 +6067,15 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + optional: true + + stringz@2.1.0: + dependencies: + char-regex: 1.0.2 + strip-ansi@5.2.0: dependencies: ansi-regex: 4.1.1 @@ -5260,6 +6088,9 @@ snapshots: strip-final-newline@3.0.0: {} + strip-json-comments@2.0.1: + optional: true + strip-json-comments@3.1.1: {} strip-literal@2.1.1: @@ -5280,10 +6111,21 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + symbol-tree@3.2.4: + optional: true + synckit@0.11.11: dependencies: '@pkgr/core': 0.2.9 + tar-fs@2.1.4: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.3 + tar-stream: 2.2.0 + optional: true + tar-fs@3.1.1: dependencies: pump: 3.0.3 @@ -5296,6 +6138,15 @@ snapshots: - bare-buffer - react-native-b4a + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + optional: true + tar-stream@3.1.7: dependencies: b4a: 1.7.3 @@ -5305,6 +6156,13 @@ snapshots: - bare-abort-controller - react-native-b4a + terser@5.44.0: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.15.0 + commander: 2.20.3 + source-map-support: 0.5.21 + test-exclude@6.0.0: dependencies: '@istanbuljs/schema': 0.1.3 @@ -5332,10 +6190,28 @@ snapshots: tinyspy@2.2.1: {} + tldts-core@7.0.17: + optional: true + + tldts@7.0.17: + dependencies: + tldts-core: 7.0.17 + optional: true + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 + tough-cookie@6.0.0: + dependencies: + tldts: 7.0.17 + optional: true + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + optional: true + ts-api-utils@1.4.3(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -5363,8 +6239,19 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 + tslib@2.5.0: {} + tslib@2.8.1: {} + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + optional: true + + type-check@0.3.2: + dependencies: + prelude-ls: 1.1.2 + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -5460,15 +6347,26 @@ snapshots: urlpattern-polyfill@10.0.0: {} + util-deprecate@1.0.2: + optional: true + + util@0.12.5: + dependencies: + inherits: 2.0.4 + is-arguments: 1.2.0 + is-generator-function: 1.1.2 + is-typed-array: 1.1.15 + which-typed-array: 1.1.19 + validator@13.15.15: {} - vite-node@1.6.1(@types/node@24.9.1): + vite-node@1.6.1(@types/node@24.9.1)(terser@5.44.0): dependencies: cac: 6.7.14 debug: 4.4.3 pathe: 1.1.2 picocolors: 1.1.1 - vite: 5.4.21(@types/node@24.9.1) + vite: 5.4.21(@types/node@24.9.1)(terser@5.44.0) transitivePeerDependencies: - '@types/node' - less @@ -5480,7 +6378,7 @@ snapshots: - supports-color - terser - vite-plugin-dts@3.9.1(@types/node@24.9.1)(rollup@4.52.5)(typescript@5.9.3)(vite@5.4.21(@types/node@24.9.1)): + vite-plugin-dts@3.9.1(@types/node@24.9.1)(rollup@4.52.5)(typescript@5.9.3)(vite@5.4.21(@types/node@24.9.1)(terser@5.44.0)): dependencies: '@microsoft/api-extractor': 7.43.0(@types/node@24.9.1) '@rollup/pluginutils': 5.3.0(rollup@4.52.5) @@ -5491,13 +6389,13 @@ snapshots: typescript: 5.9.3 vue-tsc: 1.8.27(typescript@5.9.3) optionalDependencies: - vite: 5.4.21(@types/node@24.9.1) + vite: 5.4.21(@types/node@24.9.1)(terser@5.44.0) transitivePeerDependencies: - '@types/node' - rollup - supports-color - vite@5.4.21(@types/node@24.9.1): + vite@5.4.21(@types/node@24.9.1)(terser@5.44.0): dependencies: esbuild: 0.21.5 postcss: 8.5.6 @@ -5505,8 +6403,9 @@ snapshots: optionalDependencies: '@types/node': 24.9.1 fsevents: 2.3.3 + terser: 5.44.0 - vitest@1.6.1(@types/node@24.9.1): + vitest@1.6.1(@types/node@24.9.1)(jsdom@27.1.0(canvas@3.2.0))(terser@5.44.0): dependencies: '@vitest/expect': 1.6.1 '@vitest/runner': 1.6.1 @@ -5525,11 +6424,12 @@ snapshots: strip-literal: 2.1.1 tinybench: 2.9.0 tinypool: 0.8.4 - vite: 5.4.21(@types/node@24.9.1) - vite-node: 1.6.1(@types/node@24.9.1) + vite: 5.4.21(@types/node@24.9.1)(terser@5.44.0) + vite-node: 1.6.1(@types/node@24.9.1)(terser@5.44.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.9.1 + jsdom: 27.1.0(canvas@3.2.0) transitivePeerDependencies: - less - lightningcss @@ -5552,8 +6452,30 @@ snapshots: semver: 7.7.3 typescript: 5.9.3 + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + optional: true + web-streams-polyfill@3.3.3: {} + webidl-conversions@8.0.0: + optional: true + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + optional: true + + whatwg-mimetype@4.0.0: + optional: true + + whatwg-url@15.1.0: + dependencies: + tr46: 6.0.0 + webidl-conversions: 8.0.0 + optional: true + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -5616,6 +6538,12 @@ snapshots: ws@8.18.3: {} + xml-name-validator@5.0.0: + optional: true + + xmlchars@2.2.0: + optional: true + y18n@5.0.8: {} yallist@4.0.0: {} diff --git a/src/fetchers/simpleProxy.ts b/src/fetchers/simpleProxy.ts index 6d94664..384c7ef 100644 --- a/src/fetchers/simpleProxy.ts +++ b/src/fetchers/simpleProxy.ts @@ -20,7 +20,7 @@ export function makeSimpleProxyFetcher(proxyUrl: string, f: FetchLike): Fetcher const fetcher = makeStandardFetcher(async (a, b) => { // AbortController const controller = new AbortController(); - const timeout = 15000; // 15s timeout + const timeout = 20000; // 20s timeout const timeoutId = setTimeout(() => controller.abort(), timeout); try { diff --git a/src/providers/all.ts b/src/providers/all.ts index 320afb6..e5f05a2 100644 --- a/src/providers/all.ts +++ b/src/providers/all.ts @@ -12,10 +12,8 @@ import { fsharetvScraper } from '@/providers/sources/fsharetv'; import { fsOnlineEmbeds, fsOnlineScraper } from '@/providers/sources/fsonline/index'; import { insertunitScraper } from '@/providers/sources/insertunit'; import { mp4hydraScraper } from '@/providers/sources/mp4hydra'; -import { nepuScraper } from '@/providers/sources/nepu'; import { pirxcyScraper } from '@/providers/sources/pirxcy'; import { tugaflixScraper } from '@/providers/sources/tugaflix'; -import { vidsrcScraper } from '@/providers/sources/vidsrc'; import { vidsrcvipScraper } from '@/providers/sources/vidsrcvip'; import { zoechipScraper } from '@/providers/sources/zoechip'; @@ -78,15 +76,12 @@ import { EightStreamScraper } from './sources/8stream'; import { animeflvScraper } from './sources/animeflv'; import { animetsuScraper } from './sources/animetsu'; import { cinehdplusScraper } from './sources/cinehdplus-es'; -import { cinemaosScraper } from './sources/cinemaos'; import { coitusScraper } from './sources/coitus'; import { cuevana3Scraper } from './sources/cuevana3'; import { debridScraper } from './sources/debrid'; import { embedsuScraper } from './sources/embedsu'; import { fullhdfilmizleScraper } from './sources/fullhdfilmizle'; import { hdRezkaScraper } from './sources/hdrezka'; -import { iosmirrorScraper } from './sources/iosmirror'; -import { iosmirrorPVScraper } from './sources/iosmirrorpv'; import { lookmovieScraper } from './sources/lookmovie'; import { madplayScraper } from './sources/madplay'; import { movies4fScraper } from './sources/movies4f'; @@ -123,13 +118,10 @@ export function gatherAllSources(): Array { tugaflixScraper, ee3Scraper, fsharetvScraper, - vidsrcScraper, zoechipScraper, mp4hydraScraper, embedsuScraper, slidemoviesScraper, - iosmirrorScraper, - iosmirrorPVScraper, vidapiClickScraper, coitusScraper, streamboxScraper, @@ -137,8 +129,6 @@ export function gatherAllSources(): Array { EightStreamScraper, wecimaScraper, animeflvScraper, - cinemaosScraper, - nepuScraper, pirxcyScraper, vidsrcvipScraper, madplayScraper, diff --git a/src/providers/archive/embeds/filelions.ts b/src/providers/archive/embeds/filelions.ts deleted file mode 100644 index c9ab7c7..0000000 --- a/src/providers/archive/embeds/filelions.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { flags } from '@/entrypoint/utils/targets'; -import { makeEmbed } from '@/providers/base'; - -const linkRegex = /file: ?"(http.*?)"/; -// the white space charecters may seem useless, but without them it breaks -const tracksRegex = /\{file:\s"([^"]+)",\skind:\s"thumbnails"\}/g; - -export const filelionsScraper = makeEmbed({ - id: 'filelions', - name: 'filelions', - rank: 115, - flags: [], - async scrape(ctx) { - const mainPageRes = await ctx.proxiedFetcher.full(ctx.url, { - headers: { - referer: ctx.url, - }, - }); - const mainPage = mainPageRes.body; - const mainPageUrl = new URL(mainPageRes.finalUrl); - - const streamUrl = mainPage.match(linkRegex) ?? []; - const thumbnailTrack = tracksRegex.exec(mainPage); - - const playlist = streamUrl[1]; - if (!playlist) throw new Error('Stream url not found'); - - return { - stream: [ - { - id: 'primary', - type: 'hls', - playlist, - flags: [flags.IP_LOCKED, flags.CORS_ALLOWED], - captions: [], - ...(thumbnailTrack - ? { - thumbnailTrack: { - type: 'vtt', - url: mainPageUrl.origin + thumbnailTrack[1], - }, - } - : {}), - }, - ], - }; - }, -}); diff --git a/src/providers/archive/embeds/filemoon/index.ts b/src/providers/archive/embeds/filemoon/index.ts index e16f167..940d50d 100644 --- a/src/providers/archive/embeds/filemoon/index.ts +++ b/src/providers/archive/embeds/filemoon/index.ts @@ -1,62 +1,117 @@ -// import { load } from 'cheerio'; -// import { unpack } from 'unpacker'; +import { unpack } from 'unpacker'; -// import { flags } from '@/entrypoint/utils/targets'; +import { flags } from '@/entrypoint/utils/targets'; +import { makeEmbed } from '@/providers/base'; +import { Caption, getCaptionTypeFromUrl, labelToLanguageCode } from '@/providers/captions'; -// import { SubtitleResult } from './types'; -// import { makeEmbed } from '../../base'; -// import { Caption, getCaptionTypeFromUrl, labelToLanguageCode } from '../../captions'; +import type { SubtitleResult } from './types'; -// const evalCodeRegex = /eval\((.*)\)/g; -// const fileRegex = /file:"(.*?)"/g; +const M3U8_REGEX = /https?:\/\/[^\s"'<>]+\.m3u8[^\s"'<>]*/gi; -// export const fileMoonScraper = makeEmbed({ -// id: 'filemoon', -// name: 'Filemoon', -// rank: 300, -// scrape: async (ctx) => { -// const embedRes = await ctx.proxiedFetcher(ctx.url, { -// headers: { -// referer: ctx.url, -// }, -// }); -// const embedHtml = load(embedRes); -// const evalCode = embedHtml('script').text().match(evalCodeRegex); -// if (!evalCode) throw new Error('Failed to find eval code'); -// const unpacked = unpack(evalCode[0]); -// const file = fileRegex.exec(unpacked); -// if (!file?.[1]) throw new Error('Failed to find file'); +function extractScripts(html: string): string[] { + const out: string[] = []; + const re = /]*>([\s\S]*?)<\/script>/gi; + let m: RegExpExecArray | null; + // eslint-disable-next-line no-cond-assign + while ((m = re.exec(html))) { + out.push(m[1] ?? ''); + } + return out; +} -// const url = new URL(ctx.url); -// const subtitlesLink = url.searchParams.get('sub.info'); -// const captions: Caption[] = []; -// if (subtitlesLink) { -// const captionsResult = await ctx.proxiedFetcher(subtitlesLink); +function unpackIfPacked(script: string): string | null { + try { + if (script.includes('eval(function(p,a,c,k,e,d)')) { + const once = unpack(script); + if (once && once !== script) return once; + // try one more time in case of double-pack + const twice = unpack(once ?? script); + return twice ?? once ?? null; + } + } catch { + // ignore + } + return null; +} -// for (const caption of captionsResult) { -// const language = labelToLanguageCode(caption.label); -// const captionType = getCaptionTypeFromUrl(caption.file); -// if (!language || !captionType) continue; -// captions.push({ -// id: caption.file, -// url: caption.file, -// type: captionType, -// language, -// hasCorsRestrictions: false, -// }); -// } -// } +function extractAllM3u8(text: string): string[] { + const seen: Record = {}; + const urls: string[] = []; + const body = text ?? ''; + const unescaped = body + .replace(/\\x([0-9a-fA-F]{2})/g, (_s, h) => String.fromCharCode(parseInt(h, 16))) + .replace(/\\u([0-9a-fA-F]{4})/g, (_s, h) => String.fromCharCode(parseInt(h, 16))); + const matches = unescaped.match(M3U8_REGEX) ?? []; + for (const u of matches) { + if (!seen[u]) { + seen[u] = true; + urls.push(u); + } + } + return urls; +} -// return { -// stream: [ -// { -// id: 'primary', -// type: 'hls', -// playlist: file[1], -// flags: [flags.IP_LOCKED], -// captions, -// }, -// ], -// }; -// }, -// }); +export const fileMoonScraper = makeEmbed({ + id: 'filemoon', + name: 'Filemoon', + rank: 405, + flags: [flags.IP_LOCKED], + async scrape(ctx) { + // Load initial page + const page = await ctx.proxiedFetcher.full(ctx.url, { + headers: { Referer: ctx.url }, + }); + const pageHtml = page.body; + + // Prefer iframe payload + const iframeMatch = /]+src=["']([^"']+)["']/i.exec(pageHtml); + const iframeUrl = iframeMatch ? new URL(iframeMatch[1], page.finalUrl || ctx.url).toString() : null; + + const payloadResp = iframeUrl + ? await ctx.proxiedFetcher.full(iframeUrl, { headers: { Referer: page.finalUrl || ctx.url } }) + : page; + const payloadHtml = payloadResp.body; + + // Try unpacking packed JS + const scripts = extractScripts(payloadHtml); + let collected = payloadHtml; + for (const s of scripts) { + const unpacked = unpackIfPacked(s); + if (unpacked) collected += `\n${unpacked}`; + } + + // Extract m3u8s + const m3u8s = extractAllM3u8(collected); + if (m3u8s.length === 0) throw new Error('Filemoon: no m3u8 found'); + + // Captions (optional) + const captions: Caption[] = []; + try { + const u = new URL(ctx.url); + const subtitlesLink = u.searchParams.get('sub.info'); + if (subtitlesLink) { + const res = await ctx.proxiedFetcher(subtitlesLink); + for (const c of res) { + const language = labelToLanguageCode(c.label); + const type = getCaptionTypeFromUrl(c.file); + if (!language || !type) continue; + captions.push({ id: c.file, url: c.file, type, language, hasCorsRestrictions: false }); + } + } + } catch { + // ignore caption errors + } + + return { + stream: [ + { + id: 'primary', + type: 'hls', + playlist: m3u8s[0], + flags: [flags.IP_LOCKED], + captions, + }, + ], + }; + }, +}); diff --git a/src/providers/archive/embeds/filemoon/mp4.ts b/src/providers/archive/embeds/filemoon/mp4.ts index 984691c..522d956 100644 --- a/src/providers/archive/embeds/filemoon/mp4.ts +++ b/src/providers/archive/embeds/filemoon/mp4.ts @@ -1,38 +1,36 @@ -// import { flags } from '@/entrypoint/utils/targets'; -// import { NotFoundError } from '@/utils/errors'; +import { flags } from '@/entrypoint/utils/targets'; +import { makeEmbed } from '@/providers/base'; +import { NotFoundError } from '@/utils/errors'; -// import { makeEmbed } from '../../base'; +import { fileMoonScraper } from './index'; -// import { fileMoonScraper } from './index'; +export const fileMoonMp4Scraper = makeEmbed({ + id: 'filemoon-mp4', + name: 'Filemoon MP4', + rank: 406, + flags: [flags.IP_LOCKED], + async scrape(ctx) { + const result = await fileMoonScraper.scrape(ctx); + if (!result.stream || result.stream.length === 0) throw new NotFoundError('Failed to find result'); + if (result.stream[0].type !== 'hls') throw new NotFoundError('Failed to find hls stream'); -// export const fileMoonMp4Scraper = makeEmbed({ -// id: 'filemoon-mp4', -// name: 'Filemoon MP4', -// rank: 400, -// scrape: async (ctx) => { -// const result = await fileMoonScraper.scrape(ctx); + const mp4Url = result.stream[0].playlist.replace(/\/hls2\//, '/download/').replace(/\.m3u8.*/, '.mp4'); -// if (!result.stream) throw new NotFoundError('Failed to find result'); - -// if (result.stream[0].type !== 'hls') throw new NotFoundError('Failed to find hls stream'); - -// const url = result.stream[0].playlist.replace(/\/hls2\//, '/download/').replace(/\.m3u8/, '.mp4'); - -// return { -// stream: [ -// { -// id: 'primary', -// type: 'file', -// qualities: { -// unknown: { -// type: 'mp4', -// url, -// }, -// }, -// flags: [flags.IP_LOCKED], -// captions: result.stream[0].captions, -// }, -// ], -// }; -// }, -// }); + return { + stream: [ + { + id: 'primary', + type: 'file', + qualities: { + unknown: { + type: 'mp4', + url: mp4Url, + }, + }, + flags: [flags.IP_LOCKED], + captions: result.stream[0].captions, + }, + ], + }; + }, +}); diff --git a/src/providers/archive/embeds/streambucket.ts b/src/providers/archive/embeds/streambucket.ts deleted file mode 100644 index 03f9e54..0000000 --- a/src/providers/archive/embeds/streambucket.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { flags } from '@/entrypoint/utils/targets'; -import { makeEmbed } from '@/providers/base'; - -// StreamBucket makes use of https://github.com/nicxlau/hunter-php-javascript-obfuscator - -const hunterRegex = /eval\(function\(h,u,n,t,e,r\).*?\("(.*?)",\d*?,"(.*?)",(\d*?),(\d*?),\d*?\)\)/; -const linkRegex = /file:"(.*?)"/; - -// This is a much more simple and optimized version of the "h,u,n,t,e,r" -// obfuscation algorithm. It's just basic chunked+mask encoding. -// I have seen this same encoding used on some sites under the name -// "p,l,a,y,e,r" as well -function decodeHunter(encoded: string, mask: string, charCodeOffset: number, delimiterOffset: number) { - // The encoded string is made up of 'n' number of chunks. - // Each chunk is separated by a delimiter inside the mask. - // This offset is also used as the exponentiation base in - // the charCode calculations - const delimiter = mask[delimiterOffset]; - - // Split the 'encoded' string into chunks using the delimiter, - // and filter out any empty chunks. - const chunks = encoded.split(delimiter).filter((chunk) => chunk); - - // Decode each chunk and concatenate the results to form the final 'decoded' string. - const decoded = chunks - .map((chunk) => { - // Chunks are in reverse order. 'reduceRight' removes the - // need to 'reverse' the array first - const charCode = chunk.split('').reduceRight((c, value, index) => { - // Calculate the character code for each character in the chunk. - // This involves finding the index of 'value' in the 'mask' and - // multiplying it by (delimiterOffset^position). - return c + mask.indexOf(value) * delimiterOffset ** (chunk.length - 1 - index); - }, 0); - - // The actual character code is offset by the given amount - return String.fromCharCode(charCode - charCodeOffset); - }) - .join(''); - - return decoded; -} - -export const streambucketScraper = makeEmbed({ - id: 'streambucket', - name: 'StreamBucket', - rank: 196, - // TODO - Disabled until ctx.fetcher and ctx.proxiedFetcher don't trigger bot detection - disabled: true, - flags: [], - async scrape(ctx) { - // Using the context fetchers make the site return just the string "No bots please!"? - // TODO - Fix this. Native fetch does not trigger this. No idea why right now - const response = await fetch(ctx.url); - const html = await response.text(); - - // This is different than the above mentioned bot detection - if (html.includes('captcha-checkbox')) { - // TODO - This doesn't use recaptcha, just really basic "image match". Maybe could automate? - throw new Error('StreamBucket got captchaed'); - } - - let regexResult = html.match(hunterRegex); - - if (!regexResult) { - throw new Error('Failed to find StreamBucket hunter JavaScript'); - } - - const encoded = regexResult[1]; - const mask = regexResult[2]; - const charCodeOffset = Number(regexResult[3]); - const delimiterOffset = Number(regexResult[4]); - - if (Number.isNaN(charCodeOffset)) { - throw new Error('StreamBucket hunter JavaScript charCodeOffset is not a valid number'); - } - - if (Number.isNaN(delimiterOffset)) { - throw new Error('StreamBucket hunter JavaScript delimiterOffset is not a valid number'); - } - - const decoded = decodeHunter(encoded, mask, charCodeOffset, delimiterOffset); - - regexResult = decoded.match(linkRegex); - - if (!regexResult) { - throw new Error('Failed to find StreamBucket HLS link'); - } - - return { - stream: [ - { - id: 'primary', - type: 'hls', - playlist: regexResult[1], - flags: [flags.CORS_ALLOWED], - captions: [], - }, - ], - }; - }, -}); diff --git a/src/providers/archive/embeds/vidjoy.ts b/src/providers/archive/embeds/vidjoy.ts new file mode 100644 index 0000000..e040950 --- /dev/null +++ b/src/providers/archive/embeds/vidjoy.ts @@ -0,0 +1,85 @@ +import { flags } from '@/entrypoint/utils/targets'; +import { makeEmbed } from '@/providers/base'; +import { NotFoundError } from '@/utils/errors'; +import { createM3U8ProxyUrl } from '@/utils/proxy'; + +const providers = [ + { + id: 'vidjoy-stream1', + name: 'Server 1', + rank: 110, + }, + { + id: 'vidjoy-stream2', + name: 'Server 2', + rank: 109, + }, + { + id: 'vidjoy-stream3', + name: 'Server 3', + rank: 108, + }, + { + id: 'vidjoy-stream4', + name: 'Server 4', + rank: 107, + }, + { + id: 'vidjoy-stream5', + name: 'Server 5', + rank: 106, + }, +]; + +function embed(provider: { id: string; name: string; rank: number }) { + return makeEmbed({ + id: provider.id, + name: provider.name, + rank: provider.rank, + flags: [flags.CORS_ALLOWED], + async scrape(ctx) { + // ctx.url contains the JSON stringified stream data (passed from source) + let streamData; + try { + streamData = JSON.parse(ctx.url); + } catch (error) { + throw new NotFoundError('Invalid stream data from vidjoy source'); + } + + if (!streamData.link) { + throw new NotFoundError('No stream URL found in vidjoy data'); + } + + // Validate that we have a proper URL + if (!streamData.link || streamData.link.trim() === '') { + throw new NotFoundError('Stream URL is empty'); + } + + // Create proxy URL with headers if provided + let playlistUrl = streamData.link; + if (streamData.headers && Object.keys(streamData.headers).length > 0) { + playlistUrl = createM3U8ProxyUrl(streamData.link, streamData.headers); + } + + return { + stream: [ + { + id: 'primary', + type: streamData.type || 'hls', + playlist: playlistUrl, + flags: [flags.CORS_ALLOWED], + captions: [], + }, + ], + }; + }, + }); +} + +export const [ + vidjoyStream1Scraper, + vidjoyStream2Scraper, + vidjoyStream3Scraper, + vidjoyStream4Scraper, + vidjoyStream5Scraper, +] = providers.map(embed); diff --git a/src/providers/archive/embeds/voe.ts b/src/providers/archive/embeds/voe.ts deleted file mode 100644 index 6390db3..0000000 --- a/src/providers/archive/embeds/voe.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { flags } from '@/entrypoint/utils/targets'; -import { makeEmbed } from '@/providers/base'; - -const linkRegex = /'hls': ?'(http.*?)',/; -const tracksRegex = /previewThumbnails:\s{.*src:\["([^"]+)"]/; - -export const voeScraper = makeEmbed({ - id: 'voe', - name: 'voe.sx', - rank: 180, - flags: [flags.CORS_ALLOWED, flags.IP_LOCKED], - async scrape(ctx) { - const embedRes = await ctx.proxiedFetcher.full(ctx.url); - const embed = embedRes.body; - - const playerSrc = embed.match(linkRegex) ?? []; - const thumbnailTrack = embed.match(tracksRegex); - - const streamUrl = playerSrc[1]; - if (!streamUrl) throw new Error('Stream url not found in embed code'); - - return { - stream: [ - { - type: 'hls', - id: 'primary', - playlist: streamUrl, - flags: [flags.CORS_ALLOWED, flags.IP_LOCKED], - captions: [], - headers: { - Referer: 'https://voe.sx', - }, - ...(thumbnailTrack - ? { - thumbnailTrack: { - type: 'vtt', - url: new URL(embedRes.finalUrl).origin + thumbnailTrack[1], - }, - } - : {}), - }, - ], - }; - }, -}); diff --git a/src/providers/archive/sources/.DS_Store b/src/providers/archive/sources/.DS_Store deleted file mode 100644 index ec9676c..0000000 Binary files a/src/providers/archive/sources/.DS_Store and /dev/null differ diff --git a/src/providers/sources/iosmirror.ts b/src/providers/archive/sources/iosmirror.ts similarity index 100% rename from src/providers/sources/iosmirror.ts rename to src/providers/archive/sources/iosmirror.ts diff --git a/src/providers/sources/iosmirrorpv.ts b/src/providers/archive/sources/iosmirrorpv.ts similarity index 100% rename from src/providers/sources/iosmirrorpv.ts rename to src/providers/archive/sources/iosmirrorpv.ts diff --git a/src/providers/archive/sources/m4ufree.ts b/src/providers/archive/sources/m4ufree.ts deleted file mode 100644 index 744f05f..0000000 --- a/src/providers/archive/sources/m4ufree.ts +++ /dev/null @@ -1,158 +0,0 @@ -// kinda based on m4uscraper by Paradox_77 -// thanks Paradox_77 -import { load } from 'cheerio'; - -import { SourcererEmbed, makeSourcerer } from '@/providers/base'; -import { compareMedia } from '@/utils/compare'; -import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; -import { makeCookieHeader, parseSetCookie } from '@/utils/cookie'; -import { NotFoundError } from '@/utils/errors'; - -let baseUrl = 'https://m4ufree.se'; - -const universalScraper = async (ctx: MovieScrapeContext | ShowScrapeContext) => { - // this redirects to ww1.m4ufree.tv or ww2.m4ufree.tv - // if i explicitly keep the base ww1 while the load balancers thinks ww2 is optimal - // it will keep redirecting all the requests - // not only that but the last iframe request will fail - const homePage = await ctx.proxiedFetcher.full(baseUrl); - baseUrl = new URL(homePage.finalUrl).origin; - - const searchSlug = ctx.media.title - .replace(/'/g, '') - .replace(/!|@|%|\^|\*|\(|\)|\+|=|<|>|\?|\/|,|\.|:|;|'| |"|&|#|\[|\]|~|$|_/g, '-') - .replace(/-+-/g, '-') - .replace(/^-+|-+$/g, '') - .replace(/Ă¢â‚¬â€œ/g, ''); - - const searchPage$ = load( - await ctx.proxiedFetcher(`/search/${searchSlug}.html`, { - baseUrl, - query: { - type: ctx.media.type === 'movie' ? 'movie' : 'tvs', - }, - }), - ); - - const searchResults: { title: string; year: number | undefined; url: string }[] = []; - searchPage$('.item').each((_, element) => { - const [, title, year] = - searchPage$(element) - // the title emement on their page is broken - // it just breaks when the titles are too big - .find('.imagecover a') - .attr('title') - // ex-titles: Home Alone 1990, Avengers Endgame (2019), The Curse (2023-) - ?.match(/^(.*?)\s*(?:\(?\s*(\d{4})(?:\s*-\s*\d{0,4})?\s*\)?)?\s*$/) || []; - const url = searchPage$(element).find('a').attr('href'); - - if (!title || !url) return; - - searchResults.push({ title, year: year ? parseInt(year, 10) : undefined, url }); - }); - - const watchPageUrl = searchResults.find((x) => x && compareMedia(ctx.media, x.title, x.year))?.url; - if (!watchPageUrl) throw new NotFoundError('No watchable item found'); - - ctx.progress(25); - - const watchPage = await ctx.proxiedFetcher.full(watchPageUrl, { - baseUrl, - readHeaders: ['Set-Cookie'], - }); - - ctx.progress(50); - - let watchPage$ = load(watchPage.body); - - const csrfToken = watchPage$('script:contains("_token:")') - .html() - ?.match(/_token:\s?'(.*)'/m)?.[1]; - if (!csrfToken) throw new Error('Failed to find csrfToken'); - - const laravelSession = parseSetCookie(watchPage.headers.get('Set-Cookie') ?? '').laravel_session; - if (!laravelSession?.value) throw new Error('Failed to find cookie'); - - const cookie = makeCookieHeader({ [laravelSession.name]: laravelSession.value }); - - if (ctx.media.type === 'show') { - const s = ctx.media.season.number < 10 ? `0${ctx.media.season.number}` : ctx.media.season.number.toString(); - const e = ctx.media.episode.number < 10 ? `0${ctx.media.episode.number}` : ctx.media.episode.number.toString(); - - const episodeToken = watchPage$(`button:contains("S${s}-E${e}")`).attr('idepisode'); - if (!episodeToken) throw new Error('Failed to find episodeToken'); - - watchPage$ = load( - await ctx.proxiedFetcher('/ajaxtv', { - baseUrl, - method: 'POST', - body: new URLSearchParams({ - idepisode: episodeToken, - _token: csrfToken, - }), - headers: { - cookie, - }, - }), - ); - } - - ctx.progress(75); - - const embeds: SourcererEmbed[] = []; - - const sources: { name: string; data: string }[] = watchPage$('div.row.justify-content-md-center div.le-server') - .map((_, element) => { - const name = watchPage$(element).find('span').text().toLowerCase().replace('#', ''); - const data = watchPage$(element).find('span').attr('data'); - - if (!data || !name) return null; - return { name, data }; - }) - .get(); - - for (const source of sources) { - let embedId; - - if (source.name === 'm') - embedId = 'playm4u-m'; // TODO - else if (source.name === 'nm') embedId = 'playm4u-nm'; - else if (source.name === 'h') embedId = 'hydrax'; - else continue; - - const iframePage$ = load( - await ctx.proxiedFetcher('/ajax', { - baseUrl, - method: 'POST', - body: new URLSearchParams({ - m4u: source.data, - _token: csrfToken, - }), - headers: { - cookie, - }, - }), - ); - - const url = iframePage$('iframe').attr('src')?.trim(); - if (!url) continue; - - ctx.progress(100); - - embeds.push({ embedId, url }); - } - - return { - embeds, - }; -}; - -export const m4uScraper = makeSourcerer({ - id: 'm4ufree', - name: 'M4UFree', - rank: 181, - disabled: true, - flags: [], - scrapeMovie: universalScraper, - scrapeShow: universalScraper, -}); diff --git a/src/providers/archive/sources/realdebrid/debrid.ts b/src/providers/archive/sources/realdebrid/debrid.ts new file mode 100644 index 0000000..aff3a47 --- /dev/null +++ b/src/providers/archive/sources/realdebrid/debrid.ts @@ -0,0 +1,375 @@ +/* eslint-disable no-console */ +import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; +import { NotFoundError } from '@/utils/errors'; + +interface RealDebridUnrestrictResponse { + id: string; + filename: string; + mimeType: string; + filesize: number; + link: string; + host: string; + chunks: number; + download: string; + streamable: number; +} + +interface RealDebridTorrentResponse { + id: string; + filename: string; + hash: string; + bytes: number; + host: string; + split: number; + progress: number; + status: string; + added: string; + files?: Array<{ + id: number; + path: string; + bytes: number; + selected: number; + }>; + links?: string[]; +} + +const REALDEBRID_BASE_URL = 'https://api.real-debrid.com/rest/1.0'; + +// Add a magnet link to RealDebrid +async function addMagnetToRealDebrid( + magnetUrl: string, + token: string, + ctx: MovieScrapeContext | ShowScrapeContext, +): Promise { + console.log('Adding magnet to RealDebrid:', `${magnetUrl.substring(0, 50)}...`); + + const data = await ctx.proxiedFetcher.full(`${REALDEBRID_BASE_URL}/torrents/addMagnet`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: `magnet=${encodeURIComponent(magnetUrl)}`, + }); + + if (data.statusCode !== 201 || !data.body.id) { + throw new NotFoundError('Failed to add magnet to RealDebrid'); + } + + console.log('Magnet added successfully, torrent ID:', data.body.id); + return data.body.id; +} + +// Select all files in a torrent +async function selectAllFiles( + torrentId: string, + token: string, + ctx: MovieScrapeContext | ShowScrapeContext, +): Promise { + console.log('Selecting all files for torrent:', torrentId); + + const data = await ctx.proxiedFetcher.full(`${REALDEBRID_BASE_URL}/torrents/selectFiles/${torrentId}`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: 'files=all', + }); + + if (data.statusCode !== 204 && data.statusCode !== 202) { + throw new NotFoundError('Failed to select all files for torrent'); + } + + console.log('All files selected successfully for torrent:', torrentId); +} + +// Select specific file in a torrent +async function selectSpecificFile( + torrentId: string, + fileId: number, + token: string, + ctx: MovieScrapeContext | ShowScrapeContext, +): Promise { + console.log(`Selecting specific file (${fileId}) for torrent:`, torrentId); + + const data = await ctx.proxiedFetcher.full(`${REALDEBRID_BASE_URL}/torrents/selectFiles/${torrentId}`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: `files=${fileId}`, + }); + + // Success cases: 204 (No Content) or 202 (Already Done) + if (data.statusCode !== 204 && data.statusCode !== 202) { + throw new Error(`Unexpected status code: ${data.statusCode}`); + } + + console.log(`File ${fileId} selected successfully for torrent:`, torrentId); +} + +// Get torrent info from RealDebrid +async function getTorrentInfo( + torrentId: string, + token: string, + ctx: MovieScrapeContext | ShowScrapeContext, +): Promise { + const data = await ctx.proxiedFetcher.full( + `${REALDEBRID_BASE_URL}/torrents/info/${torrentId}`, + { + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + }, + }, + ); + + if (data.statusCode === 401 || data.statusCode === 403) { + throw new NotFoundError('Failed to get torrent info'); + } + + return data.body; +} + +// Unrestrict a link on RealDebrid +async function unrestrictLink( + link: string, + token: string, + ctx: MovieScrapeContext | ShowScrapeContext, +): Promise { + console.log('Unrestricting link:', `${link.substring(0, 50)}...`); + + const data = await ctx.proxiedFetcher.full(`${REALDEBRID_BASE_URL}/unrestrict/link`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: `link=${encodeURIComponent(link)}`, + }); + + if (data.statusCode === 401 || data.statusCode === 403 || !data.body) { + throw new NotFoundError('Failed to unrestrict link'); + } + + console.log('Link unrestricted successfully, got download URL'); + return data.body; +} + +// Process a magnet link through RealDebrid and get streaming URLs +export async function getUnrestrictedLink( + magnetUrl: string, + token: string, + ctx: MovieScrapeContext | ShowScrapeContext, +): Promise { + try { + if (!magnetUrl || !magnetUrl.startsWith('magnet:?')) { + throw new NotFoundError(`Invalid magnet URL: ${magnetUrl}`); + } + + // Add the magnet to RealDebrid + const torrentId = await addMagnetToRealDebrid(magnetUrl, token, ctx); + console.log('Torrent added to RealDebrid:', torrentId); + + // Get initial torrent info + let torrentInfo = await getTorrentInfo(torrentId, token, ctx); + // console.log('Torrent info:', torrentInfo); + let waitAttempts = 0; + const maxWaitAttempts = 5; + + // First wait until the torrent is ready for file selection or another actionable state + while (waitAttempts < maxWaitAttempts) { + console.log( + `Initial torrent status (wait attempt ${waitAttempts + 1}/${maxWaitAttempts}): ${torrentInfo.status}, progress: ${torrentInfo.progress}%`, + ); + + // If the torrent is ready for file selection or already being processed, break + if ( + torrentInfo.status === 'waiting_files_selection' || + torrentInfo.status === 'downloaded' || + torrentInfo.status === 'downloading' + ) { + break; + } + + // Check for error states + const errorStatuses = ['error', 'virus', 'dead', 'magnet_error', 'magnet_conversion']; + if (errorStatuses.includes(torrentInfo.status)) { + throw new NotFoundError(`Torrent processing failed with status: ${torrentInfo.status}`); + } + + // Wait 2 seconds before checking again + await new Promise((resolve) => { + setTimeout(resolve, 2000); + }); + waitAttempts++; + + // Get updated torrent info + torrentInfo = await getTorrentInfo(torrentId, token, ctx); + // console.log('Torrent info attempt 2:', torrentInfo); + } + + // Select files based on status + if (torrentInfo.status === 'waiting_files_selection') { + // If we have files info, try to select the best MP4 file + if (torrentInfo.files && torrentInfo.files.length > 0) { + const mp4Files = torrentInfo.files.filter((file) => file.path.toLowerCase().endsWith('.mp4')); + + if (mp4Files.length > 0) { + console.log(`Found ${mp4Files.length} MP4 files, attempting to match title`); + const cleanMediaTitle = ctx.media.title.toLowerCase(); + const firstWord = cleanMediaTitle.split(' ')[0]; + + // Try exact title match first + const exactMatches = mp4Files.filter((file) => { + const cleanFileName = file.path.split('/').pop()?.toLowerCase().replace(/\./g, ' ') || ''; + return cleanFileName.includes(cleanMediaTitle); + }); + + let selectedFile; + if (exactMatches.length > 0) { + console.log(`Found ${exactMatches.length} files exactly matching title "${ctx.media.title}"`); + selectedFile = exactMatches.reduce((largest, current) => + current.bytes > largest.bytes ? current : largest, + ); + } else { + // Try matching first word + const firstWordMatches = mp4Files.filter((file) => { + return file.path.includes(firstWord); + }); + + if (firstWordMatches.length > 0) { + console.log(`Found ${firstWordMatches.length} files matching first word "${firstWord}"`); + selectedFile = firstWordMatches.reduce((largest, current) => + current.bytes > largest.bytes ? current : largest, + ); + } else { + // If no matching files, select the largest MP4 + console.log(`No matching files found, selecting largest MP4`); + selectedFile = mp4Files.reduce((largest, current) => (current.bytes > largest.bytes ? current : largest)); + } + } + const largestMp4 = selectedFile; + + // Select only this specific file + await selectSpecificFile(torrentId, largestMp4.id, token, ctx); + console.log('Selected specific file:', largestMp4.id); + } else { + // If no MP4 files, select all files + await selectAllFiles(torrentId, token, ctx); + console.log('Selected all files'); + } + } else { + // If no file info available, select all files + await selectAllFiles(torrentId, token, ctx); + console.log('Selected all files'); + } + } else if (torrentInfo.status !== 'downloaded') { + // For any other non-completed status, select all files + await selectAllFiles(torrentId, token, ctx); + console.log('Selected all files'); + } + + // Wait for the torrent to be processed + let attempts = 0; + const maxAttempts = 30; // 60 seconds max wait time (2s * 30) + const validCompletedStatuses = ['downloaded', 'ready']; + const errorStatuses = ['error', 'virus', 'dead', 'magnet_error', 'magnet_conversion']; + + console.log('Waiting for torrent to be processed...'); + + while (attempts < maxAttempts) { + torrentInfo = await getTorrentInfo(torrentId, token, ctx); + console.log( + `Torrent status (attempt ${attempts + 1}/${maxAttempts}): ${torrentInfo.status}, progress: ${torrentInfo.progress}%`, + ); + + // Check if torrent is ready + if (validCompletedStatuses.includes(torrentInfo.status) && torrentInfo.links && torrentInfo.links.length > 0) { + let targetLink = torrentInfo.links[0]; + + // If there are files, try to find the largest MP4 + if (torrentInfo.files) { + const mp4Files = torrentInfo.files.filter( + (file) => file.path.toLowerCase().endsWith('.mp4') && file.selected === 1, + ); + + if (mp4Files.length > 0) { + console.log(`Found ${mp4Files.length} MP4 files, selecting largest`); + const largestMp4 = mp4Files.reduce((largest, current) => + current.bytes > largest.bytes ? current : largest, + ); + + const linkIndex = torrentInfo.files.findIndex((f) => f.id === largestMp4.id); + if (linkIndex !== -1 && torrentInfo.links[linkIndex]) { + targetLink = torrentInfo.links[linkIndex]; + console.log(`Selected largest MP4: ${largestMp4.path}, size: ${largestMp4.bytes} bytes`); + } + } else { + console.log('No MP4 files found, using first available link'); + } + } + + // Unrestrict the link + const unrestrictedData = await unrestrictLink(targetLink, token, ctx); + return unrestrictedData.download; + } + + // Check for error states + if (errorStatuses.includes(torrentInfo.status)) { + throw new NotFoundError(`Torrent processing failed with status: ${torrentInfo.status}`); + } + + // If torrent is stuck in downloading with 0% progress for a while, try to re-select files + if (torrentInfo.status === 'downloading' && torrentInfo.progress === 0 && attempts > 5 && attempts % 5 === 0) { + console.log('Torrent seems stuck at 0%, trying to re-select files...'); + + // Get fresh torrent info + torrentInfo = await getTorrentInfo(torrentId, token, ctx); + + if (torrentInfo.files && torrentInfo.files.length > 0) { + const mp4Files = torrentInfo.files.filter((file) => file.path.toLowerCase().endsWith('.mp4')); + + if (mp4Files.length > 0) { + const largestMp4 = mp4Files.reduce((largest, current) => + current.bytes > largest.bytes ? current : largest, + ); + + // Re-select this specific file + await selectSpecificFile(torrentId, largestMp4.id, token, ctx); + } else { + // If no MP4 files, re-select all files + await selectAllFiles(torrentId, token, ctx); + } + } else { + // If no file info available, re-select all files + await selectAllFiles(torrentId, token, ctx); + } + } + + // Special case: if we have reached 100% progress but status isn't "downloaded" yet + if (torrentInfo.progress === 100 && attempts > 10) { + console.log('Torrent is at 100% but status is not completed. Checking for links anyway...'); + if (torrentInfo.links && torrentInfo.links.length > 0) { + const unrestrictedData = await unrestrictLink(torrentInfo.links[0], token, ctx); + return unrestrictedData.download; + } + } + + // Wait 2 seconds before checking again + // eslint-disable-next-line no-await-in-loop + await new Promise((resolve) => { + setTimeout(resolve, 2000); + }); + attempts++; + } + + throw new NotFoundError(`Timeout waiting for torrent to be processed after ${maxAttempts * 2} seconds`); + } catch (error: unknown) { + if (error instanceof NotFoundError) throw error; + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + throw new NotFoundError(`Error processing magnet link: ${errorMessage}`); + } +} diff --git a/src/providers/archive/sources/realdebrid/index.ts b/src/providers/archive/sources/realdebrid/index.ts new file mode 100644 index 0000000..f994f9b --- /dev/null +++ b/src/providers/archive/sources/realdebrid/index.ts @@ -0,0 +1,159 @@ +/* eslint-disable no-console */ +import { flags } from '@/entrypoint/utils/targets'; +import { SourcererOutput, makeSourcerer } from '@/providers/base'; +import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; +import { NotFoundError } from '@/utils/errors'; + +import { getUnrestrictedLink } from './debrid'; +import { getTorrents } from './torrentio'; + +const OVERRIDE_TOKEN = ''; + +const getRealDebridToken = (): string | null => { + try { + if (OVERRIDE_TOKEN) return OVERRIDE_TOKEN; + } catch { + // Ignore + } + try { + if (typeof window === 'undefined') return null; + const prefData = window.localStorage.getItem('__MW::preferences'); + if (!prefData) return null; + const parsedAuth = JSON.parse(prefData); + return parsedAuth?.state?.realDebridKey || null; + } catch (e) { + console.error('Error getting RealDebrid token:', e); + return null; + } +}; + +async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise { + const apiToken = getRealDebridToken(); + if (!apiToken) { + throw new NotFoundError('RealDebrid API token is required'); + } + if (!ctx.media.imdbId) { + throw new NotFoundError('IMDB ID required'); + } + + // Get torrent magnet links from Torrentio + const streams = await getTorrents(ctx); + // console.log('streams', streams); + + ctx.progress(20); + + // Process each magnet link through RealDebrid in batches + const maxConcurrentQualities = 2; + const qualities = Object.keys(streams); + const processedStreams: Array<{ quality: string; url: string } | null> = []; + + // Process qualities in batches to avoid too many concurrent requests + for (let i = 0; i < qualities.length; i += maxConcurrentQualities) { + const batch = qualities.slice(i, i + maxConcurrentQualities); + // progress + const progressStart = 40; + const progressEnd = 90; + const progressPerBatch = (progressEnd - progressStart) / Math.ceil(qualities.length / maxConcurrentQualities); + const currentBatchProgress = progressStart + progressPerBatch * (i / maxConcurrentQualities); + ctx.progress(Math.round(currentBatchProgress)); + + const batchPromises = batch.map((quality) => { + const magnetUrl = streams[quality]; + return getUnrestrictedLink(magnetUrl, apiToken, ctx) + .then((downloadUrl) => ({ quality, url: downloadUrl })) + .catch((error) => { + console.error(`Failed to process ${quality} stream:`, error); + return null; + }); + }); + + if (batchPromises.length > 0) { + const batchResults = await Promise.all(batchPromises); + processedStreams.push(...batchResults); + } + } + + // Filter out failed streams and create quality map + const filteredStreams = processedStreams + .filter((stream): stream is { quality: string; url: string } => stream !== null) + .filter((stream) => stream.url.toLowerCase().endsWith('.mp4')) // only mp4 + .reduce((acc: Record, { quality, url }) => { + // Normalize quality format for the output + let qualityKey: number | string; + if (quality === '4K') { + qualityKey = 2160; + } else { + qualityKey = parseInt(quality.replace('P', ''), 10); + } + if (Number.isNaN(qualityKey)) qualityKey = 'unknown'; + + acc[qualityKey] = url; + return acc; + }, {}); + + if (Object.keys(filteredStreams).length === 0) { + throw new NotFoundError('No suitable streams found'); + } + + ctx.progress(100); + + return { + stream: [ + { + id: 'primary', + captions: [], + qualities: { + ...(filteredStreams[2160] && { + '4k': { + type: 'mp4', + url: filteredStreams[2160], + }, + }), + ...(filteredStreams[1080] && { + 1080: { + type: 'mp4', + url: filteredStreams[1080], + }, + }), + ...(filteredStreams[720] && { + 720: { + type: 'mp4', + url: filteredStreams[720], + }, + }), + ...(filteredStreams[480] && { + 480: { + type: 'mp4', + url: filteredStreams[480], + }, + }), + ...(filteredStreams[360] && { + 360: { + type: 'mp4', + url: filteredStreams[360], + }, + }), + ...(filteredStreams.unknown && { + unknown: { + type: 'mp4', + url: filteredStreams.unknown, + }, + }), + }, + type: 'file', + flags: [flags.CORS_ALLOWED], + }, + ], + embeds: [], + }; +} + +export const realDebridScraper = makeSourcerer({ + id: 'realdebrid', + name: 'RealDebrid (Beta)', + rank: 280, + disabled: !getRealDebridToken(), + flags: [flags.CORS_ALLOWED], + scrapeMovie: comboScraper, + scrapeShow: comboScraper, +}); diff --git a/src/providers/archive/sources/realdebrid/torrentio.ts b/src/providers/archive/sources/realdebrid/torrentio.ts new file mode 100644 index 0000000..e7874e5 --- /dev/null +++ b/src/providers/archive/sources/realdebrid/torrentio.ts @@ -0,0 +1,151 @@ +/* eslint-disable no-console */ +import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; +import { NotFoundError } from '@/utils/errors'; + +interface TorrentStream { + name: string; + title: string; + url: string; + infoHash?: string; + sources?: string[]; + behaviorHints?: { + bingeGroup?: string; + countryWhitelist?: string[]; + filename?: string; + }; +} + +export interface QualityTorrents { + [quality: string]: string; +} + +// Filter out cam/ts/screener versions +function isAcceptableQuality(torrentName: string): boolean { + const lowerName = torrentName.toLowerCase(); + const bannedTerms = [ + 'cam', + 'camrip', + 'hdcam', + 'ts', + 'telesync', + 'hdts', + 'dvdscr', + 'screener', + 'scr', + 'r5', + 'workprint', + ]; + + return !bannedTerms.some((term) => lowerName.includes(term)); +} + +// Extract quality from torrent name +function extractQuality(torrentName: string): string { + const name = torrentName.toLowerCase(); + + if (name.includes('2160p') || name.includes('4k')) return '4K'; + if (name.includes('1080p')) return '1080P'; + if (name.includes('720p')) return '720P'; + if (name.includes('480p')) return '480P'; + if (name.includes('360p')) return '360P'; + + return 'unknown'; +} + +// Process torrents and group by quality +function processTorrents(streams: TorrentStream[]): QualityTorrents { + // Filter out bad quality torrents + const goodQualityStreams = streams.filter((stream) => isAcceptableQuality(stream.name)); + const filteredStreams = goodQualityStreams.filter( + (stream) => + stream.title?.toLowerCase().includes('mp4') || stream.behaviorHints?.filename?.toLowerCase().includes('mp4'), + ); + + if (filteredStreams.length === 0) { + throw new NotFoundError('No usable torrents found'); + } + + // if (filteredStreams.length > 0) { + // console.log('sample stream:', JSON.stringify(filteredStreams[0], null, 2)); // eslint-disable-line no-console + // } + + // Group torrents by quality + const qualityGroups: { [quality: string]: TorrentStream[] } = {}; + + for (const stream of filteredStreams) { + const quality = extractQuality(stream.name); + if (!qualityGroups[quality]) { + qualityGroups[quality] = []; + } + qualityGroups[quality].push(stream); + } + + // Select the best torrent for each quality (we just pick the first one) + const result: QualityTorrents = {}; + + for (const [quality, torrentStreams] of Object.entries(qualityGroups)) { + if (torrentStreams.length > 0) { + // Check if the URL is a magnet link, if not create one using infoHash if available + const stream = torrentStreams[0]; + let magnetUrl = stream.url; + + if (!magnetUrl && stream.infoHash) { + // Create a magnet link from the infoHash + magnetUrl = `magnet:?xt=urn:btih:${stream.infoHash}&dn=${encodeURIComponent(stream.name)}`; + } + + if (magnetUrl) { + result[quality] = magnetUrl; + } + } + } + + console.log('processed qualities:', Object.keys(result)); + for (const [quality, url] of Object.entries(result)) { + console.log(`${quality}: ${url.substring(0, 30)}...`); + } + + return result; +} + +// Function to get torrents from Torrentio service +export async function getTorrents(ctx: MovieScrapeContext | ShowScrapeContext): Promise { + if (!ctx.media.imdbId) { + throw new NotFoundError('IMDB ID required'); + } + + const imdbId = ctx.media.imdbId; + let searchPath: string; + + if (ctx.media.type === 'show') { + const seasonNumber = ctx.media.season.number; + const episodeNumber = ctx.media.episode.number; + searchPath = `series/${imdbId}:${seasonNumber}:${episodeNumber}.json`; + } else { + searchPath = `movie/${imdbId}.json`; + } + + try { + const torrentioUrl = `https://torrentio.strem.fun/providers=yts,eztv,rarbg,1337x,thepiratebay,kickasstorrents,torrentgalaxy,magnetdl/stream/${searchPath}`; + console.log('Fetching torrents from:', torrentioUrl); + + const response = await ctx.fetcher.full(torrentioUrl); + + if (response.statusCode !== 200) { + throw new NotFoundError(`Failed to fetch torrents: ${response.statusCode} ${response.body}`); + } + + console.log('Found torrents:', response.body.streams?.length || 0); + + if (!response.body.streams || response.body.streams.length === 0) { + throw new NotFoundError('No streams found'); + } + + // Process and group torrents by quality + return processTorrents(response.body.streams); + } catch (error: unknown) { + if (error instanceof NotFoundError) throw error; + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + throw new NotFoundError(`Error fetching torrents: ${errorMessage}`); + } +} diff --git a/src/providers/archive/sources/vidjoy.ts b/src/providers/archive/sources/vidjoy.ts new file mode 100644 index 0000000..404b85b --- /dev/null +++ b/src/providers/archive/sources/vidjoy.ts @@ -0,0 +1,95 @@ +/* eslint-disable no-console */ +import CryptoJS from 'crypto-js'; + +import { flags } from '@/entrypoint/utils/targets'; +import { SourcererEmbed, SourcererOutput, makeSourcerer } from '@/providers/base'; +import { MovieScrapeContext } from '@/utils/context'; +import { NotFoundError } from '@/utils/errors'; + +const baseUrl = 'https://vidjoy.pro'; +const decryptionKey = '029f3936fb744c4512e66d3a8150c6129472ccdff5b0dd5ec6e512fc06194ef1'; + +async function comboScraper(ctx: MovieScrapeContext): Promise { + let apiUrl = `${baseUrl}/embed/api/fastfetch2/${ctx.media.tmdbId}?sr=0`; + + let streamRes = await ctx.proxiedFetcher.full(apiUrl, { + method: 'GET', + headers: { + referer: 'https://vidjoy.pro/', + origin: 'https://vidjoy.pro', + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + }, + }); + + if (streamRes.statusCode !== 200) { + apiUrl = `${baseUrl}/embed/api/fetch2/${ctx.media.tmdbId}?srName=Modread&sr=0`; + streamRes = await ctx.proxiedFetcher.full(apiUrl, { + method: 'GET', + headers: { + referer: 'https://vidjoy.pro/', + origin: 'https://vidjoy.pro', + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + }, + }); + } + + if (streamRes.statusCode !== 200) { + throw new NotFoundError('Failed to fetch video source from both endpoints'); + } + + ctx.progress(50); + + const encryptedData = streamRes.body; + const decrypted = CryptoJS.AES.decrypt(encryptedData, decryptionKey).toString(CryptoJS.enc.Utf8); + + if (!decrypted) { + throw new NotFoundError('Failed to decrypt video source'); + } + + ctx.progress(70); + + let parsedData; + try { + parsedData = JSON.parse(decrypted); + } catch (error) { + console.error('JSON parsing error:', error); + console.error('Decrypted data:', decrypted.substring(0, 200)); + throw new NotFoundError('Failed to parse decrypted video data'); + } + + if (!parsedData.url || !Array.isArray(parsedData.url) || parsedData.url.length === 0) { + throw new NotFoundError('No video URLs found in response'); + } + + ctx.progress(90); + + const embeds: SourcererEmbed[] = []; + + // Create embeds for each available stream + parsedData.url.forEach((urlData: any, index: number) => { + embeds.push({ + embedId: `vidjoy-stream${index + 1}`, + url: JSON.stringify({ + link: urlData.link, + type: urlData.type || 'hls', + lang: urlData.lang || 'English', + headers: parsedData.headers || {}, + }), + }); + }); + + return { + embeds, + }; +} + +export const vidjoyScraper = makeSourcerer({ + id: 'vidjoy', + name: 'vidjoy 🔥', + rank: 185, + disabled: true, + flags: [flags.CORS_ALLOWED], + scrapeMovie: comboScraper, +}); diff --git a/src/providers/archive/upcloud.ts b/src/providers/archive/upcloud.ts new file mode 100644 index 0000000..20e09f6 --- /dev/null +++ b/src/providers/archive/upcloud.ts @@ -0,0 +1,142 @@ +import crypto from 'crypto-js'; + +import { flags } from '@/entrypoint/utils/targets'; +import { makeEmbed } from '@/providers/base'; +import { Caption, getCaptionTypeFromUrl, labelToLanguageCode } from '@/providers/captions'; + +const origin = 'https://rabbitstream.net'; +const referer = 'https://rabbitstream.net/'; + +const { AES, enc } = crypto; + +interface StreamRes { + server: number; + sources: string; + tracks: { + file: string; + kind: 'captions' | 'thumbnails'; + label: string; + }[]; +} + +function isJSON(json: string) { + try { + JSON.parse(json); + return true; + } catch { + return false; + } +} + +/* +example script segment: +switch(I9){case 0x0:II=X,IM=t;break;case 0x1:II=b,IM=D;break;case 0x2:II=x,IM=f;break;case 0x3:II=S,IM=j;break;case 0x4:II=U,IM=G;break;case 0x5:II=partKeyStartPosition_5,IM=partKeyLength_5;} +*/ +function extractKey(script: string): [number, number][] | null { + const startOfSwitch = script.lastIndexOf('switch'); + const endOfCases = script.indexOf('partKeyStartPosition'); + const switchBody = script.slice(startOfSwitch, endOfCases); + + const nums: [number, number][] = []; + const matches = switchBody.matchAll(/:[a-zA-Z0-9]+=([a-zA-Z0-9]+),[a-zA-Z0-9]+=([a-zA-Z0-9]+);/g); + for (const match of matches) { + const innerNumbers: number[] = []; + for (const varMatch of [match[1], match[2]]) { + const regex = new RegExp(`${varMatch}=0x([a-zA-Z0-9]+)`, 'g'); + const varMatches = [...script.matchAll(regex)]; + const lastMatch = varMatches[varMatches.length - 1]; + if (!lastMatch) return null; + const number = parseInt(lastMatch[1], 16); + innerNumbers.push(number); + } + + nums.push([innerNumbers[0], innerNumbers[1]]); + } + + return nums; +} + +export const upcloudScraper = makeEmbed({ + id: 'upcloud', + name: 'UpCloud', + rank: 200, + disabled: true, + flags: [flags.CORS_ALLOWED], + async scrape(ctx) { + // Example url: https://dokicloud.one/embed-4/{id}?z= + const parsedUrl = new URL(ctx.url.replace('embed-5', 'embed-4')); + + const dataPath = parsedUrl.pathname.split('/'); + const dataId = dataPath[dataPath.length - 1]; + + const streamRes = await ctx.proxiedFetcher(`${parsedUrl.origin}/ajax/embed-4/getSources?id=${dataId}`, { + headers: { + Referer: parsedUrl.origin, + 'X-Requested-With': 'XMLHttpRequest', + }, + }); + + let sources: { file: string; type: string } | null = null; + + if (!isJSON(streamRes.sources)) { + const scriptJs = await ctx.proxiedFetcher(`https://rabbitstream.net/js/player/prod/e4-player.min.js`, { + query: { + // browser side caching on this endpoint is quite extreme. Add version query paramter to circumvent any caching + v: Date.now().toString(), + }, + }); + const decryptionKey = extractKey(scriptJs); + if (!decryptionKey) throw new Error('Key extraction failed'); + + let extractedKey = ''; + let strippedSources = streamRes.sources; + let totalledOffset = 0; + decryptionKey.forEach(([a, b]) => { + const start = a + totalledOffset; + const end = start + b; + extractedKey += streamRes.sources.slice(start, end); + strippedSources = strippedSources.replace(streamRes.sources.substring(start, end), ''); + totalledOffset += b; + }); + + const decryptedStream = AES.decrypt(strippedSources, extractedKey).toString(enc.Utf8); + const parsedStream = JSON.parse(decryptedStream)[0]; + if (!parsedStream) throw new Error('No stream found'); + sources = parsedStream; + } + + if (!sources) throw new Error('upcloud source not found'); + + const captions: Caption[] = []; + streamRes.tracks.forEach((track) => { + if (track.kind !== 'captions') return; + const type = getCaptionTypeFromUrl(track.file); + if (!type) return; + const language = labelToLanguageCode(track.label.split(' ')[0]); + if (!language) return; + captions.push({ + id: track.file, + language, + hasCorsRestrictions: false, + type, + url: track.file, + }); + }); + + return { + stream: [ + { + id: 'primary', + type: 'hls', + playlist: sources.file, + flags: [flags.CORS_ALLOWED], + captions, + preferredHeaders: { + Referer: referer, + Origin: origin, + }, + }, + ], + }; + }, +}); diff --git a/src/providers/embeds/.DS_Store b/src/providers/embeds/.DS_Store deleted file mode 100644 index 9bc3674..0000000 Binary files a/src/providers/embeds/.DS_Store and /dev/null differ diff --git a/src/providers/embeds/filelions.ts b/src/providers/embeds/filelions.ts index 2eaf6ef..f2d30f8 100644 --- a/src/providers/embeds/filelions.ts +++ b/src/providers/embeds/filelions.ts @@ -50,6 +50,9 @@ export const filelionsScraper = makeEmbed({ id: 'primary', type: 'hls', playlist: streamUrl, + headers: { + Referer: 'https://primesrc.me/', + }, flags: [], captions: [], }, diff --git a/src/providers/embeds/filemoon.ts b/src/providers/embeds/filemoon.ts index 7b0cc16..1cabef8 100644 --- a/src/providers/embeds/filemoon.ts +++ b/src/providers/embeds/filemoon.ts @@ -1,7 +1,6 @@ import { load } from 'cheerio'; import { unpack } from 'unpacker'; -import { flags } from '@/entrypoint/utils/targets'; import { makeEmbed } from '@/providers/base'; import { NotFoundError } from '@/utils/errors'; @@ -12,7 +11,7 @@ export const filemoonScraper = makeEmbed({ id: 'filemoon', name: 'Filemoon', rank: 405, - flags: [flags.CORS_ALLOWED], + flags: [], async scrape(ctx) { const headers = { Accept: @@ -68,13 +67,8 @@ export const filemoonScraper = makeEmbed({ stream: [ { id: 'primary', - type: 'file', - qualities: { - unknown: { - type: 'mp4', - url: videoUrl, - }, - }, + type: 'hls', + playlist: videoUrl, headers: { Referer: `${new URL(ctx.url).origin}/`, 'User-Agent': userAgent, diff --git a/src/providers/embeds/streambucket.ts b/src/providers/embeds/streambucket.ts new file mode 100644 index 0000000..39d3c19 --- /dev/null +++ b/src/providers/embeds/streambucket.ts @@ -0,0 +1,203 @@ +import { load } from 'cheerio'; + +import { flags } from '@/entrypoint/utils/targets'; +import { makeEmbed } from '@/providers/base'; +import { EmbedScrapeContext } from '@/utils/context'; +import { NotFoundError } from '@/utils/errors'; +import { createM3U8ProxyUrl } from '@/utils/proxy'; + +export const streambucketScraper = makeEmbed({ + id: 'streambucket', + name: 'Streambucket', + rank: 220, + disabled: true, + flags: [flags.CORS_ALLOWED], + async scrape(ctx: EmbedScrapeContext) { + // Handle redirects for multiembed/streambucket URLs + let baseUrl = ctx.url; + if (baseUrl.includes('multiembed') || baseUrl.includes('ghostplayer')) { + const redirectResp = await ctx.proxiedFetcher.full(baseUrl); + baseUrl = redirectResp.finalUrl; + } + + const userAgent = + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Mobile Safari/537.36'; + + ctx.progress(20); + + // Prepare POST data for requesting the page + const data = { + 'button-click': 'ZEhKMVpTLVF0LVBTLVF0LVAtMGs1TFMtUXpPREF0TC0wLVYzTi0wVS1RTi0wQTFORGN6TmprLTU=', + 'button-referer': '', + }; + + // Send POST request to fetch initial response + const postResp = await ctx.proxiedFetcher(baseUrl, { + method: 'POST', + body: new URLSearchParams(data), + headers: { + Referer: 'https://multiembed.mov', + 'X-Requested-With': 'XMLHttpRequest', + 'User-Agent': userAgent, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }); + + ctx.progress(40); + + // Extract the session token required to fetch sources + const tokenMatch = postResp.match(/load_sources\("([^"]+)"\)/); + if (!tokenMatch) throw new NotFoundError('Session token not found'); + const token = tokenMatch[1]; + + // Request the sources list using the extracted token + const sourcesResp = await ctx.proxiedFetcher('https://streamingnow.mov/response.php', { + method: 'POST', + body: new URLSearchParams({ token }), + headers: { + Referer: baseUrl, + 'X-Requested-With': 'XMLHttpRequest', + 'User-Agent': userAgent, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }); + + ctx.progress(60); + + const $ = load(sourcesResp); + + // Try to find VIP source first, then fallback to other sources + let selectedSource = $('li') + .filter((_, el) => { + const text = $(el).text().toLowerCase(); + return text.includes('vipstream-s'); + }) + .first(); + + // If no VIP, try other high-quality sources + if (!selectedSource.length) { + // Try vidsrc first as fallback + selectedSource = $('li') + .filter((_, el) => { + const text = $(el).text().toLowerCase(); + return text.includes('vidsrc'); + }) + .first(); + + // If no vidsrc, try any stream source + if (!selectedSource.length) { + selectedSource = $('li').first(); + } + } + + if (!selectedSource.length) { + throw new NotFoundError('No streams available'); + } + + // Extract server and video IDs from the selected source element + const serverId = selectedSource.attr('data-server'); + const videoId = selectedSource.attr('data-id'); + + if (!serverId || !videoId) { + throw new NotFoundError('Server or video ID not found'); + } + + ctx.progress(70); + + // Fetch VIP streaming page HTML + const vipUrl = `https://streamingnow.mov/playvideo.php?video_id=${videoId}&server_id=${serverId}&token=${token}&init=1`; + const vipResp = await ctx.proxiedFetcher(vipUrl, { + headers: { + Referer: baseUrl, + 'User-Agent': userAgent, + }, + }); + + ctx.progress(80); + + // Check if response contains CAPTCHA (anti-bot protection) + const hasCaptcha = + vipResp.includes('captcha') || vipResp.includes('Verification') || vipResp.includes('Select all images'); + + if (hasCaptcha) { + throw new NotFoundError('Stream protected by CAPTCHA'); + } + + // Look for direct video file URL in the response + const directVideoMatch = + vipResp.match(/file:"(https?:\/\/[^"]+)"/) || + vipResp.match(/"(https?:\/\/[^"]+\.m3u8[^"]*)"/) || + vipResp.match(/"(https?:\/\/[^"]+\.mp4[^"]*)"/); + if (directVideoMatch) { + const videoUrl = directVideoMatch[1]; + + // Extract domain for referer + const urlObj = new URL(baseUrl); + const defaultDomain = `${urlObj.protocol}//${urlObj.host}`; + + const headers = { + Referer: defaultDomain, + 'User-Agent': userAgent, + }; + + return { + stream: [ + { + id: 'primary', + type: 'hls', + playlist: createM3U8ProxyUrl(videoUrl, { requires: [flags.CORS_ALLOWED], disallowed: [] }, headers), + headers, + flags: [flags.CORS_ALLOWED], + captions: [], + }, + ], + }; + } + + // Fallback to iframe method + const vip$ = load(vipResp); + const iframe = vip$('iframe.source-frame.show'); + if (!iframe.length) throw new NotFoundError('Video iframe not found'); + + const iframeUrl = iframe.attr('src'); + if (!iframeUrl) throw new NotFoundError('Iframe URL not found'); + + // Get video page + const videoResp = await ctx.proxiedFetcher(iframeUrl, { + headers: { + Referer: vipUrl, + 'User-Agent': userAgent, + }, + }); + + ctx.progress(90); + + // Extract video URL + const videoMatch = videoResp.match(/file:"(https?:\/\/[^"]+)"/); + if (!videoMatch) throw new NotFoundError('Video URL not found'); + + const videoUrl = videoMatch[1]; + + // Extract domain for referer + const urlObj = new URL(baseUrl); + const defaultDomain = `${urlObj.protocol}//${urlObj.host}`; + + const headers = { + Referer: defaultDomain, + 'User-Agent': userAgent, + }; + + return { + stream: [ + { + id: 'primary', + type: 'hls', + playlist: createM3U8ProxyUrl(videoUrl, { requires: [flags.CORS_ALLOWED], disallowed: [] }, headers), + headers, + flags: [flags.CORS_ALLOWED], + captions: [], + }, + ], + }; + }, +}); diff --git a/src/providers/sources/.DS_Store b/src/providers/sources/.DS_Store deleted file mode 100644 index d752853..0000000 Binary files a/src/providers/sources/.DS_Store and /dev/null differ diff --git a/src/providers/sources/animeflv.ts b/src/providers/sources/animeflv.ts index 2fd3bed..901957c 100644 --- a/src/providers/sources/animeflv.ts +++ b/src/providers/sources/animeflv.ts @@ -194,7 +194,7 @@ export const animeflvScraper = makeSourcerer({ id: 'animeflv', name: 'AnimeFLV', rank: 90, - disabled: true, + disabled: false, flags: [flags.CORS_ALLOWED], scrapeShow: comboScraper, scrapeMovie: comboScraper, diff --git a/src/providers/sources/cinemaos.ts b/src/providers/sources/cinemaos.ts deleted file mode 100644 index 3248744..0000000 --- a/src/providers/sources/cinemaos.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { flags } from '@/entrypoint/utils/targets'; -import { SourcererOutput, makeSourcerer } from '@/providers/base'; -import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; - -// const baseUrl = atob('aHR0cHM6Ly9jaW5lbWFvcy12My52ZXJjZWwuYXBwLw=='); - -const CINEMAOS_SERVERS = [ - // 'flowcast', - 'shadow', - 'asiacloud', - // 'hindicast', - // 'anime', - // 'animez', - // 'guard', - // 'hq', - // 'ninja', - // 'alpha', - // 'kaze', - // 'zenith', - // 'cast', - // 'ghost', - // 'halo', - // 'kinoecho', - // 'ee3', - // 'volt', - // 'putafilme', - 'ophim', - // 'kage', -]; - -async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise { - const embeds = []; - - const query: any = { - type: ctx.media.type, - tmdbId: ctx.media.tmdbId, - }; - - if (ctx.media.type === 'show') { - query.season = ctx.media.season.number; - query.episode = ctx.media.episode.number; - } - - // // V2 Embeds / Hexa - // try { - // const hexaUrl = `api/hexa?type=${query.type}&tmdbId=${query.tmdbId}`; - // const hexaRes = await ctx.proxiedFetcher(hexaUrl, { baseUrl }); - // const hexaData = typeof hexaRes === 'string' ? JSON.parse(hexaRes) : hexaRes; - // if (hexaData && hexaData.sources && typeof hexaData.sources === 'object') { - // for (const [key, value] of Object.entries(hexaData.sources)) { - // if (value && value.url) { - // embeds.push({ - // embedId: `cinemaos-hexa-${key}`, - // url: JSON.stringify({ ...query, service: `hexa-${key}`, directUrl: value.url }), - // }); - // } - // } - // } - // } catch (e: any) { - // // eslint-disable-next-line no-console - // console.error('Failed to fetch hexa sources'); - // } - - // V3 Embeds - for (const server of CINEMAOS_SERVERS) { - embeds.push({ - embedId: `cinemaos-${server}`, - url: JSON.stringify({ ...query, service: server }), - }); - } - - ctx.progress(50); - - return { embeds }; -} - -export const cinemaosScraper = makeSourcerer({ - id: 'cinemaos', - name: 'CinemaOS', - rank: 149, - disabled: true, - flags: [flags.CORS_ALLOWED], - scrapeMovie: comboScraper, - scrapeShow: comboScraper, -}); diff --git a/src/providers/sources/consumet/index.ts b/src/providers/sources/consumet/index.ts new file mode 100644 index 0000000..509416b --- /dev/null +++ b/src/providers/sources/consumet/index.ts @@ -0,0 +1,73 @@ +import { flags } from '@/entrypoint/utils/targets'; +import { SourcererOutput, makeSourcerer } from '@/providers/base'; +import { ShowScrapeContext } from '@/utils/context'; + +import { InfoResponse, SearchResponse } from './types'; + +async function consumetScraper(ctx: ShowScrapeContext): Promise { + // Search + const searchQuery = ctx.media.title; + const page = 1; + + const searchUrl = `https://api.1anime.app/anime/zoro/${encodeURIComponent(searchQuery)}?page=${page}`; + const searchResponse = await ctx.fetcher(searchUrl); + + if (!searchResponse?.results?.length) { + throw new Error('No results found'); + } + + const bestMatch = + searchResponse.results.find((result) => result.title.toLowerCase() === ctx.media.title.toLowerCase()) || + searchResponse.results[0]; + + // Get episode list + const infoUrl = `https://api.1anime.app/anime/zoro/info?id=${bestMatch.id}`; + const infoResponse = await ctx.fetcher(infoUrl); + + if (!infoResponse?.episodes?.length) { + throw new Error('No episodes found'); + } + + const targetEpisode = infoResponse.episodes.find((ep) => ep.number === ctx.media.episode.number); + + if (!targetEpisode) { + throw new Error('Episode not found'); + } + + // Parse embeds + const query = { + episodeId: `${bestMatch.id}$${ctx.media.season.number}$${targetEpisode.id}$both`, + }; + + const embeds = [ + { + embedId: 'consumet-vidcloud', + url: JSON.stringify({ ...query, server: 'vidcloud' }), + }, + { + embedId: 'consumet-streamsb', + url: JSON.stringify({ ...query, server: 'streamsb' }), + }, + { + embedId: 'consumet-vidstreaming', + url: JSON.stringify({ ...query, server: 'vidstreaming' }), + }, + { + embedId: 'consumet-streamtape', + url: JSON.stringify({ ...query, server: 'streamtape' }), + }, + ]; + + return { + embeds, + }; +} + +export const ConsumetScraper = makeSourcerer({ + id: 'consumet', + name: 'Consumet (Anime) 🔥', + rank: 5, + disabled: true, + flags: [flags.CORS_ALLOWED], + scrapeShow: consumetScraper, +}); diff --git a/src/providers/sources/consumet/types.ts b/src/providers/sources/consumet/types.ts new file mode 100644 index 0000000..11bd741 --- /dev/null +++ b/src/providers/sources/consumet/types.ts @@ -0,0 +1,36 @@ +export interface SearchResult { + id: string; + title: string; + image: string; + releaseDate: string | null; + subOrDub: 'sub' | 'dub'; +} + +export interface SearchResponse { + totalPages: number; + currentPage: number; + hasNextPage: boolean; + results: SearchResult[]; +} + +export interface Episode { + id: string; + number: number; + url: string; +} + +export interface InfoResponse { + id: string; + title: string; + url: string; + image: string; + releaseDate: string | null; + description: string | null; + genres: string[]; + subOrDub: 'sub' | 'dub'; + type: string | null; + status: string; + otherName: string | null; + totalEpisodes: number; + episodes: Episode[]; +} diff --git a/src/providers/sources/debrid/comet.ts b/src/providers/sources/debrid/comet.ts new file mode 100644 index 0000000..287c890 --- /dev/null +++ b/src/providers/sources/debrid/comet.ts @@ -0,0 +1,42 @@ +import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; + +import { getAddonStreams, parseStreamData } from './helpers'; +import { DebridParsedStream, debridProviders } from './types'; + +export async function getCometStreams( + token: string, + debridProvider: debridProviders, + ctx: MovieScrapeContext | ShowScrapeContext, +): Promise { + const cometBaseUrl = 'https://comet.elfhosted.com'; // Free instance sponsored by ElfHosted, but you can customize it to your liking. + // If you're unfamiliar with Stremio addons, basically stremio addons are just api endpoints, and so they have to encode the config in the url to be able to have a config that works with stremio + // So this just constructs the user's config for Comet. It could be customized to your liking as well! + const cometConfig = btoa( + JSON.stringify({ + maxResultsPerResolution: 0, + maxSize: 0, + cachedOnly: false, + removeTrash: true, + resultFormat: ['all'], + debridService: debridProvider, + debridApiKey: token, + debridStreamProxyPassword: '', + languages: { exclude: [], preferred: ['en'] }, + resolutions: {}, + options: { remove_ranks_under: -10000000000, allow_english_in_languages: false, remove_unknown_languages: false }, + }), + ); + + const cometStreamsRaw = (await getAddonStreams(`${cometBaseUrl}/${cometConfig}`, ctx)).streams; + const newStreams: { title: string; url: string }[] = []; + + for (let i = 0; i < cometStreamsRaw.length; i++) { + if (cometStreamsRaw[i].description !== undefined) + newStreams.push({ + title: (cometStreamsRaw[i].description as string).replace(/\n/g, ''), + url: cometStreamsRaw[i].url, + }); + } + const parsedData = await parseStreamData(newStreams, ctx); + return parsedData; +} diff --git a/src/providers/sources/debrid/helpers.ts b/src/providers/sources/debrid/helpers.ts new file mode 100644 index 0000000..e139de0 --- /dev/null +++ b/src/providers/sources/debrid/helpers.ts @@ -0,0 +1,48 @@ +// Helpers for Stremio addon API Formats + +import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; + +import { DebridParsedStream, stremioStream } from './types'; + +export async function getAddonStreams( + addonUrl: string, + ctx: MovieScrapeContext | ShowScrapeContext, +): Promise<{ streams: stremioStream[] }> { + if (!ctx.media.imdbId) { + throw new Error('Error: ctx.media.imdbId is required.'); + } + let addonResponse: { streams: stremioStream[] } | undefined; + + if (ctx.media.type === 'show') { + addonResponse = await ctx.proxiedFetcher( + `${addonUrl}/stream/series/${ctx.media.imdbId}:${ctx.media.season.number}:${ctx.media.episode.number}.json`, + ); + } else { + addonResponse = await ctx.proxiedFetcher(`${addonUrl}/stream/movie/${ctx.media.imdbId}.json`); + } + + if (!addonResponse) { + throw new Error('Error: addon did not respond'); + } + + return addonResponse; +} + +interface StreamInput { + title: string; + url: string; + [key: string]: any; +} + +export async function parseStreamData( + streams: StreamInput[], + ctx: MovieScrapeContext | ShowScrapeContext, +): Promise { + return ctx.proxiedFetcher('https://torrent-parse.pstream.mov', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(streams), + }); +} diff --git a/src/providers/sources/debrid/index.ts b/src/providers/sources/debrid/index.ts index 9fec00a..9f49c47 100644 --- a/src/providers/sources/debrid/index.ts +++ b/src/providers/sources/debrid/index.ts @@ -4,10 +4,12 @@ import { SourcererOutput, makeSourcerer } from '@/providers/base'; import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; import { NotFoundError } from '@/utils/errors'; -import { debridProviders, torrentioResponse } from './types'; +import { getCometStreams } from './comet'; +import { getAddonStreams, parseStreamData } from './helpers'; +import { DebridParsedStream, debridProviders } from './types'; const OVERRIDE_TOKEN = ''; -const OVERRIDE_SERVICE = ''; // torbox or realdebrid (or real-debrid) +const OVERRIDE_SERVICE: debridProviders | '' = ''; // torbox or realdebrid (or real-debrid) const getDebridToken = (): string | null => { try { @@ -48,27 +50,6 @@ const getDebridService = (): debridProviders => { } }; -type DebridParsedStream = { - resolution?: string; - year?: number; - source?: string; - bitDepth?: string; - codec?: string; - audio?: string; - container?: string; - seasons?: number[]; - season?: number; - episodes?: number[]; - episode?: number; - complete?: boolean; - unrated?: boolean; - remastered?: boolean; - languages?: string[]; - dubbed?: boolean; - title: string; - url: string; -}; - function normalizeQuality(resolution?: string): '4k' | 1080 | 720 | 480 | 360 | 'unknown' { if (!resolution) return 'unknown'; const res = resolution.toLowerCase(); @@ -104,51 +85,46 @@ async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promis const debridProvider: debridProviders = getDebridService(); - let torrentioUrl = `https://torrentio.strem.fun/${debridProvider}=${apiKey}/stream/`; - - if (ctx.media.type === 'show') { - torrentioUrl += `series/${ctx.media.imdbId}:${ctx.media.season.number}:${ctx.media.episode.number}.json`; - } else { - torrentioUrl += `movie/${ctx.media.imdbId}.json`; - } - const torrentioData = (await ctx.proxiedFetcher(torrentioUrl)) as torrentioResponse; - - const torrentioStreams = torrentioData?.streams || []; - if (torrentioStreams.length === 0) { - console.log('No torrents found', torrentioData); - throw new NotFoundError('No torrents found'); - } + const [torrentioResult, cometStreams] = await Promise.all([ + getAddonStreams(`https://torrentio.strem.fun/${debridProvider}=${apiKey}`, ctx), + getCometStreams(apiKey, debridProvider, ctx).catch(() => { + return [] as DebridParsedStream[]; + }), + ]); ctx.progress(33); - const response: DebridParsedStream[] = await ctx.proxiedFetcher('https://torrent-parse.pstream.mov/', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(torrentioStreams), - }); - if (response.length === 0) { - console.log('No streams found or parse failed!', response); + const torrentioStreams = await parseStreamData( + torrentioResult.streams.map((s) => ({ + ...s, + title: s.title ?? '', + })), + ctx, + ); + + const allStreams = [...torrentioStreams, ...cometStreams]; + + if (allStreams.length === 0) { + console.log('No streams found from either source!'); throw new NotFoundError('No streams found or parse failed!'); } + console.log( + `Total streams: ${allStreams.length} (${torrentioStreams.length} from Torrentio, ${cometStreams.length} from Comet)`, + ); + ctx.progress(66); - // Group by quality, pick the most compatible stream for each const qualities: Partial> = {}; - // Group streams by normalized quality const byQuality: Record = {}; - for (const stream of response) { + for (const stream of allStreams) { const quality = normalizeQuality(stream.resolution); if (!byQuality[quality]) byQuality[quality] = []; byQuality[quality].push(stream); } - // For each quality, pick the best compatible stream (prefer mp4+aac, fallback to mkv) for (const [quality, streams] of Object.entries(byQuality)) { - // Prefer mp4 + aac const mp4Aac = streams.find((s) => s.container === 'mp4' && s.audio === 'aac'); if (mp4Aac) { qualities[quality as keyof typeof qualities] = { @@ -157,7 +133,6 @@ async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promis }; continue; } - // Fallback: any mp4 const mp4 = streams.find((s) => s.container === 'mp4'); if (mp4) { qualities[quality as keyof typeof qualities] = { @@ -166,6 +141,7 @@ async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promis }; continue; } + streams.sort((a, b) => scoreStream(b) - scoreStream(a)); const best = streams[0]; if (best) { diff --git a/src/providers/sources/debrid/types.ts b/src/providers/sources/debrid/types.ts index 601a8f1..61db59b 100644 --- a/src/providers/sources/debrid/types.ts +++ b/src/providers/sources/debrid/types.ts @@ -3,7 +3,9 @@ export type debridProviders = 'torbox' | 'real-debrid'; export interface stremioStream { name: string; - title: string; + description?: string; + title?: string; + url: string; infoHash: string; fileIdx: number; behaviorHints?: { @@ -15,9 +17,31 @@ export interface stremioStream { sources?: string[]; } -export interface torrentioResponse { - streams: stremioStream[]; +export type stremioAddonStreamResponse = Promise<{ streams: stremioStream[]; [key: string]: any }>; + +export type torrentioResponse = Awaited & { cacheMaxAge: number; staleRevalidate: number; staleError: number; -} +}; + +export type DebridParsedStream = { + resolution?: string; + year?: number; + source?: string; + bitDepth?: string; + codec?: string; + audio?: string; + container?: string; + seasons?: number[]; + season?: number; + episodes?: number[]; + episode?: number; + complete?: boolean; + unrated?: boolean; + remastered?: boolean; + languages?: string[]; + dubbed?: boolean; + title: string; + url: string; +}; diff --git a/src/providers/sources/dopebox/index.ts b/src/providers/sources/dopebox/index.ts index b7bed0a..7992267 100644 --- a/src/providers/sources/dopebox/index.ts +++ b/src/providers/sources/dopebox/index.ts @@ -80,7 +80,7 @@ async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promis export const dopeboxScraper = makeSourcerer({ id: 'dopebox', name: 'Dopebox', - rank: 210, + rank: 197, flags: ['cors-allowed'], scrapeMovie: comboScraper, scrapeShow: comboScraper, diff --git a/src/providers/sources/fsharetv.ts b/src/providers/sources/fsharetv.ts index 8d8a2ea..d651b55 100644 --- a/src/providers/sources/fsharetv.ts +++ b/src/providers/sources/fsharetv.ts @@ -90,7 +90,7 @@ async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promis export const fsharetvScraper = makeSourcerer({ id: 'fsharetv', name: 'FshareTV', - rank: 190, + rank: 200, flags: [], scrapeMovie: comboScraper, }); diff --git a/src/providers/sources/fsonline/index.ts b/src/providers/sources/fsonline/index.ts index 8b444cd..cdfeb89 100644 --- a/src/providers/sources/fsonline/index.ts +++ b/src/providers/sources/fsonline/index.ts @@ -121,7 +121,7 @@ async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promis export const fsOnlineScraper = makeSourcerer({ id: 'fsonline', name: 'FSOnline', - rank: 200, + rank: 189, flags: ['cors-allowed'], scrapeMovie: comboScraper, scrapeShow: comboScraper, diff --git a/src/providers/sources/m4ufree.ts b/src/providers/sources/m4ufree.ts new file mode 100644 index 0000000..261e871 --- /dev/null +++ b/src/providers/sources/m4ufree.ts @@ -0,0 +1,244 @@ +/* eslint-disable no-console */ +import { load } from 'cheerio'; +import CryptoJS from 'crypto-js'; + +import { flags } from '@/entrypoint/utils/targets'; +import { makeSourcerer } from '@/providers/base'; +import { compareMedia } from '@/utils/compare'; +import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; +import { getSetCookieHeader, makeCookieHeader, parseSetCookie } from '@/utils/cookie'; +import { NotFoundError } from '@/utils/errors'; +import { createM3U8ProxyUrl } from '@/utils/proxy'; + +let baseUrl = 'https://m4ufree.page'; + +// AES Decryption Keys +// They rotate these keys occasionally. If it brakes chances are they rotated keys +// To get the keys, go on the site and find the req to something like: +// https://if6.ppzj-youtube.cfd/play/6119b02e9c0163458e94808e/648ff94023c354ba9eb2221d2ec81040.html +// Find the obfuscated script section, deobfuscate with webcrack, you'll find the keys +const KEYS = { + IDFILE_KEY: 'jcLycoRJT6OWjoWspgLMOZwS3aSS0lEn', + IDUSER_KEY: 'PZZ3J3LDbLT0GY7qSA5wW5vchqgpO36O', + REQUEST_KEY: 'vlVbUQhkOhoSfyteyzGeeDzU0BHoeTyZ', + RESPONSE_KEY: 'oJwmvmVBajMaRCTklxbfjavpQO7SZpsL', + MD5_SALT: 'KRWN3AdgmxEMcd2vLN1ju9qKe8Feco5h', +} as const; + +function decryptHexToUtf8(encryptedHex: string, key: string): string { + const hexParsed = CryptoJS.enc.Hex.parse(encryptedHex); + const base64String = hexParsed.toString(CryptoJS.enc.Base64); + const decrypted = CryptoJS.AES.decrypt(base64String, key); + const result = decrypted.toString(CryptoJS.enc.Utf8); + return result; +} + +function encryptUtf8ToHex(plainText: string, key: string): string { + const encrypted = CryptoJS.AES.encrypt(plainText, key).toString(); + const base64Parsed = CryptoJS.enc.Base64.parse(encrypted); + const result = base64Parsed.toString(CryptoJS.enc.Hex); + return result; +} + +async function fetchIframeUrl( + ctx: MovieScrapeContext | ShowScrapeContext, + watchPageHtml: string, + csrfToken: string, + cookie: string, + referer: string, +): Promise { + const $ = load(watchPageHtml); + + // Movie vs TV handling + let pageHtml = watchPageHtml; + + if (ctx.media.type === 'show') { + const seasonPadded = String(ctx.media.season.number).padStart(2, '0'); + const episodePadded = String(ctx.media.episode.number).padStart(2, '0'); + const idepisode = $(`button:contains("S${seasonPadded}-E${episodePadded}")`).attr('idepisode'); + if (!idepisode) throw new NotFoundError('idepisode not found'); + + // Load TV episode block + pageHtml = await ctx.proxiedFetcher('/ajaxtv', { + baseUrl, + method: 'POST', + body: new URLSearchParams({ idepisode, _token: csrfToken }), + headers: { + Cookie: cookie, + Referer: referer, + 'X-Requested-With': 'XMLHttpRequest', + }, + }); + } + + const $$ = load(pageHtml); + const playhqData = $$('#playhq.singlemv.active').attr('data'); + if (!playhqData) throw new NotFoundError('playhq data not found'); + + // Request iframe wrapper + const iframeWrapper = await ctx.proxiedFetcher('/ajax', { + baseUrl, + method: 'POST', + body: new URLSearchParams({ m4u: playhqData, _token: csrfToken }), + headers: { + Cookie: cookie, + Referer: referer, + 'X-Requested-With': 'XMLHttpRequest', + }, + }); + + const $$$ = load(iframeWrapper); + const iframeUrl = $$$('iframe').attr('src'); + if (!iframeUrl) throw new NotFoundError('iframe src not found'); + + return iframeUrl.startsWith('http') ? iframeUrl : new URL(iframeUrl, baseUrl).toString(); +} + +async function extractM3u8FromIframe(ctx: MovieScrapeContext | ShowScrapeContext, iframeUrl: string): Promise { + const iframeHtml = await ctx.proxiedFetcher(iframeUrl, { + headers: { + Referer: baseUrl, + }, + }); + + const idfileEnc = iframeHtml.match(/const\s+idfile_enc\s*=\s*"([^"]+)"/)?.[1]; + const idUserEnc = iframeHtml.match(/const\s+idUser_enc\s*=\s*"([^"]+)"/)?.[1]; + const domainApi = iframeHtml.match(/const\s+DOMAIN_API\s*=\s*'([^']+)'/)?.[1]; + + if (!idfileEnc || !idUserEnc || !domainApi) throw new NotFoundError('Required data not found in iframe HTML'); + + const idfile = decryptHexToUtf8(idfileEnc, KEYS.IDFILE_KEY); + const iduser = decryptHexToUtf8(idUserEnc, KEYS.IDUSER_KEY); + + const requestData = { + idfile, + iduser, + domain_play: 'https://my.playhq.net', + platform: 'Win32', + hlsSupport: true, + jwplayer: {}, + } as const; + + const encryptedData = encryptUtf8ToHex(JSON.stringify(requestData), KEYS.REQUEST_KEY); + const md5Hash = CryptoJS.MD5(encryptedData + KEYS.MD5_SALT).toString(); + + const responseBody = await ctx.proxiedFetcher(`${domainApi}/playiframe`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Accept: 'application/json, text/javascript, */*; q=0.01', + 'X-Requested-With': 'XMLHttpRequest', + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', + Referer: iframeUrl, + Origin: new URL(domainApi).origin, + }, + body: `data=${encryptedData}|${md5Hash}`, + }); + + let json: any; + try { + json = typeof responseBody === 'string' ? JSON.parse(responseBody) : responseBody; + } catch { + throw new NotFoundError('Invalid JSON from playiframe'); + } + + if (json?.status === 1 && json?.type === 'url-m3u8-encv1' && typeof json.data === 'string') { + const decryptedUrl = decryptHexToUtf8(json.data, KEYS.RESPONSE_KEY); + if (!decryptedUrl) throw new NotFoundError('Failed to decrypt stream URL'); + return decryptedUrl; + } + + throw new NotFoundError(json?.msg || 'Failed to get stream URL'); +} + +const comboScraper = async (ctx: MovieScrapeContext | ShowScrapeContext) => { + // Normalize base by following any redirects + const home = await ctx.proxiedFetcher.full(baseUrl); + baseUrl = new URL(home.finalUrl).origin; + + // Build search slug from title + const searchSlug = ctx.media.title + .replace(/'/g, '') + .replace(/!|@|%|\^|\*|\(|\)|\+|=|<|>|\?|\/|,|\.|:|;|'| |"|&|#|\[|\]|~|$|_/g, '-') + .replace(/-+-/g, '-') + .replace(/^-+|-+$/g, '') + .replace(/Ă¢â‚¬â€œ/g, ''); + + const searchPageHtml = await ctx.proxiedFetcher(`/search/${searchSlug}.html`, { + baseUrl, + query: { + type: ctx.media.type === 'movie' ? 'movie' : 'tvs', + }, + }); + const searchPage$ = load(searchPageHtml); + + const results: { title: string; year: number | undefined; url: string }[] = []; + searchPage$('.item').each((_, el) => { + const [, title, year] = + searchPage$(el) + .find('.imagecover a') + .attr('title') + ?.match(/^(.*?)\s*(?:\(?\s*(\d{4})(?:\s*-\s*\d{0,4})?\s*\)?)?\s*$/) || []; + const url = searchPage$(el).find('a').attr('href'); + if (!title || !url) return; + results.push({ title, year: year ? parseInt(year, 10) : undefined, url }); + }); + + const watchPath = results.find((x) => x && compareMedia(ctx.media, x.title, x.year))?.url; + if (!watchPath) throw new NotFoundError('No watchable item found'); + + const watchFinal = await ctx.proxiedFetcher.full(watchPath, { + baseUrl, + readHeaders: ['Set-Cookie'], + }); + const watchHtml = watchFinal.body; + const watchUrl = new URL(watchFinal.finalUrl).toString(); + + const csrfToken = load(watchHtml).root().find('meta[name="csrf-token"]').attr('content'); + if (!csrfToken) throw new NotFoundError('Token not found'); + + const cookies = parseSetCookie(getSetCookieHeader(watchFinal.headers)); + const laravel = cookies.laravel_session; + if (!laravel?.value) throw new NotFoundError('Session cookie not found'); + const cookieHeader = makeCookieHeader({ [laravel.name]: laravel.value }); + + ctx.progress(50); + + const iframeUrl = await fetchIframeUrl(ctx, watchHtml, csrfToken, cookieHeader, watchUrl); + const m3u8Url = await extractM3u8FromIframe(ctx, iframeUrl); + + ctx.progress(90); + + // The stream headers aren't required, but they are used to trigger the extension to be used since the stream is only cors locked. + // BUT we are using the M3U8 proxy to bypass the cors lock, so we shouldn't remove the flag. + // We don't have handling for only cors locked streams with the extension. + const streamHeaders = { + Referer: baseUrl, + Origin: baseUrl, + }; + + return { + embeds: [], + stream: [ + { + id: 'primary', + type: 'hls' as const, + playlist: createM3U8ProxyUrl(m3u8Url, ctx.features, streamHeaders), + headers: streamHeaders, + flags: [flags.CORS_ALLOWED], + captions: [], + }, + ], + }; +}; + +export const m4ufreeScraper = makeSourcerer({ + id: 'm4ufree', + name: 'M4UFree 🔥', + rank: 182, + disabled: true, + flags: [flags.CORS_ALLOWED], + scrapeMovie: comboScraper, + scrapeShow: comboScraper, +}); diff --git a/src/providers/sources/multiembed.ts b/src/providers/sources/multiembed.ts new file mode 100644 index 0000000..6e13227 --- /dev/null +++ b/src/providers/sources/multiembed.ts @@ -0,0 +1,37 @@ +/* eslint-disable no-console */ +import { flags } from '@/entrypoint/utils/targets'; +import { SourcererOutput, makeSourcerer } from '@/providers/base'; +import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; + +const baseUrl = 'https://multiembed.mov'; + +async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise { + ctx.progress(50); + + let url: string; + + if (ctx.media.type === 'show') { + url = `${baseUrl}/?video_id=${ctx.media.imdbId}&s=${ctx.media.season.number}&e=${ctx.media.episode.number}`; + } else { + url = `${baseUrl}/?video_id=${ctx.media.imdbId}`; + } + + return { + embeds: [ + { + embedId: 'streambucket', + url, + }, + ], + }; +} + +export const multiembedScraper = makeSourcerer({ + id: 'multiembed', + name: 'MultiEmbed 🔥', + rank: 145, + disabled: true, + flags: [flags.CORS_ALLOWED], + scrapeMovie: comboScraper, + scrapeShow: comboScraper, +}); diff --git a/src/providers/sources/nepu.ts b/src/providers/sources/nepu.ts deleted file mode 100644 index b126a84..0000000 --- a/src/providers/sources/nepu.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { flags } from '@/entrypoint/utils/targets'; -import { SourcererOutput, makeSourcerer } from '@/providers/base'; -import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; -import { NotFoundError } from '@/utils/errors'; - -const nepuBase = 'https://nscrape.andresdev.org/api'; - -async function scrape(ctx: MovieScrapeContext | ShowScrapeContext): Promise { - const tmdbId = ctx.media.tmdbId; - - let url: string; - if (ctx.media.type === 'movie') { - url = `${nepuBase}/get-stream?tmdbId=${tmdbId}`; - } else { - url = `${nepuBase}/get-show-stream?tmdbId=${tmdbId}&season=${ctx.media.season.number}&episode=${ctx.media.episode.number}`; - } - - const response = await ctx.proxiedFetcher(url); - - if (!response.success || !response.rurl) { - throw new NotFoundError('No stream found'); - } - - return { - stream: [ - { - id: 'nepu', - type: 'hls', - playlist: response.rurl, - flags: [flags.CORS_ALLOWED], - captions: [], - }, - ], - embeds: [], - }; -} - -export const nepuScraper = makeSourcerer({ - id: 'nepu', - name: 'Nepu', - rank: 201, - disabled: true, - flags: [flags.CORS_ALLOWED], - scrapeMovie: scrape, - scrapeShow: scrape, -}); diff --git a/src/providers/sources/pirxcy.ts b/src/providers/sources/pirxcy.ts index f2d29e3..5ad23d9 100644 --- a/src/providers/sources/pirxcy.ts +++ b/src/providers/sources/pirxcy.ts @@ -181,7 +181,7 @@ async function scrapeShow(ctx: ShowScrapeContext): Promise { export const pirxcyScraper = makeSourcerer({ id: 'pirxcy', name: 'Pirxcy', - rank: 230, + rank: 290, disabled: true, flags: [flags.CORS_ALLOWED], scrapeMovie, diff --git a/src/providers/sources/primesrc.ts b/src/providers/sources/primesrc.ts new file mode 100644 index 0000000..0d19d7b --- /dev/null +++ b/src/providers/sources/primesrc.ts @@ -0,0 +1,74 @@ +import { SourcererOutput, makeSourcerer } from '@/providers/base'; +import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; +import { NotFoundError } from '@/utils/errors'; + +async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise { + const baseApiUrl = 'https://primesrc.me/api/v1/'; + + let serverData; + try { + if (ctx.media.type === 'movie') { + const url = `${baseApiUrl}s?tmdb=${ctx.media.tmdbId}&type=movie`; + serverData = await fetch(url); + } else { + const url = `${baseApiUrl}s?tmdb=${ctx.media.tmdbId}&season=${ctx.media.season.number}&episode=${ctx.media.episode.number}&type=tv`; + serverData = await fetch(url); + } + } catch (error) { + return { embeds: [] }; + } + + let data; + try { + data = await serverData.json(); + } catch (error) { + return { embeds: [] }; + } + + const nameToEmbedId: Record = { + Filelions: 'filelions', + Dood: 'dood', + Streamwish: 'streamwish-english', + Filemoon: 'filemoon', + }; + + if (!data.servers || !Array.isArray(data.servers)) { + return { embeds: [] }; + } + + const embeds = []; + for (const server of data.servers) { + if (!server.name || !server.key) { + continue; + } + if (nameToEmbedId[server.name]) { + try { + const linkData = await fetch(`${baseApiUrl}l?key=${server.key}`); + if (linkData.status !== 200) { + continue; + } + const linkJson = await linkData.json(); + if (linkJson.link) { + const embed = { + embedId: nameToEmbedId[server.name], + url: linkJson.link, + }; + embeds.push(embed); + } + } catch (error) { + throw new NotFoundError(`Error: ${error}`); + } + } + } + + return { embeds }; +} + +export const primesrcScraper = makeSourcerer({ + id: 'primesrc', + name: 'PrimeSrc', + rank: 105, + flags: [], + scrapeMovie: comboScraper, + scrapeShow: comboScraper, +}); diff --git a/src/providers/sources/primewire.ts b/src/providers/sources/primewire.ts index b3fc7da..a2f779a 100644 --- a/src/providers/sources/primewire.ts +++ b/src/providers/sources/primewire.ts @@ -1,74 +1,123 @@ -import { SourcererOutput, makeSourcerer } from '@/providers/base'; +import { flags } from '@/entrypoint/utils/targets'; +import { SourcererEmbed, SourcererOutput, makeSourcerer } from '@/providers/base'; import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; import { NotFoundError } from '@/utils/errors'; +interface PStreamResponse { + imdb_id: string; + streams: Array<{ + headers: Record; + link: string; + quality: string; + server: string; + type: string; + }>; + title: string; +} + async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise { - const baseApiUrl = 'https://primesrc.me/api/v1/'; + // Build the API URL based on media type + let apiUrl: string; + if (ctx.media.type === 'movie') { + // For movies, we need IMDB ID + if (!ctx.media.imdbId) throw new NotFoundError('IMDB ID required for movies'); + apiUrl = `https://primewire.pstream.mov/movie/${ctx.media.imdbId}`; + } else { + // For TV shows, we need IMDB ID, season, and episode + if (!ctx.media.imdbId) throw new NotFoundError('IMDB ID required for TV shows'); + apiUrl = `https://primewire.pstream.mov/tv/${ctx.media.imdbId}/${ctx.media.season.number}/${ctx.media.episode.number}`; + } - let serverData; - try { - if (ctx.media.type === 'movie') { - const url = `${baseApiUrl}s?tmdb=${ctx.media.tmdbId}&type=movie`; - serverData = await fetch(url); + ctx.progress(30); + + // Fetch the stream data + const response = await ctx.fetcher(apiUrl, { + headers: { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36', + }, + }); + + if (!response.streams || !Array.isArray(response.streams) || response.streams.length === 0) { + throw new NotFoundError('No streams found'); + } + + ctx.progress(60); + + // Process each stream as a separate embed using server-mirrors + const embeds: SourcererEmbed[] = []; + + for (const stream of response.streams) { + if (!stream.link || !stream.quality) continue; + + let mirrorContext: any; + + if (stream.type === 'm3u8') { + // Handle HLS streams + mirrorContext = { + type: 'hls', + stream: stream.link, + headers: stream.headers || [], + captions: [], + flags: !stream.headers || Object.keys(stream.headers).length === 0 ? [flags.CORS_ALLOWED] : [], + }; } else { - const url = `${baseApiUrl}s?tmdb=${ctx.media.tmdbId}&season=${ctx.media.season.number}&episode=${ctx.media.episode.number}&type=tv`; - serverData = await fetch(url); - } - } catch (error) { - return { embeds: [] }; - } - - let data; - try { - data = await serverData.json(); - } catch (error) { - return { embeds: [] }; - } - - const nameToEmbedId: Record = { - Filelions: 'filelions', - Dood: 'dood', - Streamwish: 'streamwish-english', - Filemoon: 'filemoon', - }; - - if (!data.servers || !Array.isArray(data.servers)) { - return { embeds: [] }; - } - - const embeds = []; - for (const server of data.servers) { - if (!server.name || !server.key) { - continue; - } - if (nameToEmbedId[server.name]) { - try { - const linkData = await fetch(`${baseApiUrl}l?key=${server.key}`); - if (linkData.status !== 200) { - continue; + // Handle file streams + // Convert quality string to numeric key for the qualities object + let qualityKey: string; + if (stream.quality === 'ORG') { + // Handle original quality - check if it's an MP4 + const urlPath = stream.link.split('?')[0]; + if (urlPath.toLowerCase().endsWith('.mp4')) { + qualityKey = 'unknown'; + } else { + continue; // Skip non-MP4 original quality } - const linkJson = await linkData.json(); - if (linkJson.link) { - const embed = { - embedId: nameToEmbedId[server.name], - url: linkJson.link, - }; - embeds.push(embed); - } - } catch (error) { - throw new NotFoundError(`Error: ${error}`); + } else if (stream.quality === '4K') { + qualityKey = '4k'; + } else { + // Parse numeric qualities like "720", "1080", etc. + const parsed = parseInt(stream.quality.replace('P', ''), 10); + if (Number.isNaN(parsed)) continue; + qualityKey = parsed.toString(); } + + // Create the mirror context for server-mirrors embed + mirrorContext = { + type: 'file', + qualities: { + [qualityKey === 'unknown' || qualityKey === '4k' ? qualityKey : parseInt(qualityKey, 10)]: { + type: 'mp4', + url: stream.link, + }, + }, + flags: !stream.headers || Object.keys(stream.headers).length === 0 ? [flags.CORS_ALLOWED] : [], + headers: stream.headers || [], + captions: [], + }; } + + embeds.push({ + embedId: 'mirror', + url: JSON.stringify(mirrorContext), + }); } + if (embeds.length === 0) { + throw new NotFoundError('No valid streams found'); + } + + ctx.progress(90); + return { embeds }; } export const primewireScraper = makeSourcerer({ id: 'primewire', - name: 'PrimeWire', - rank: 105, - flags: [], + name: 'PrimeWire 🔥', + rank: 206, + disabled: true, + flags: [flags.CORS_ALLOWED], scrapeMovie: comboScraper, scrapeShow: comboScraper, }); diff --git a/src/providers/sources/vidify.ts b/src/providers/sources/vidify.ts index 0ac9f2b..edd61a5 100644 --- a/src/providers/sources/vidify.ts +++ b/src/providers/sources/vidify.ts @@ -1,71 +1,50 @@ +import { flags } from '@/entrypoint/utils/targets'; import { SourcererOutput, makeSourcerer } from '@/providers/base'; import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; +const VIDIFY_SERVERS = [ + { name: 'Mbox', sr: 17 }, + { name: 'Xprime', sr: 15 }, + { name: 'Hexo', sr: 8 }, + { name: 'Prime', sr: 9 }, + { name: 'Nitro', sr: 20 }, + { name: 'Meta', sr: 6 }, + { name: 'Veasy', sr: 16 }, + { name: 'Lux', sr: 26 }, + { name: 'Vfast', sr: 11 }, + { name: 'Zozo', sr: 7 }, + { name: 'Tamil', sr: 13 }, + { name: 'Telugu', sr: 14 }, + { name: 'Beta', sr: 5 }, + { name: 'Alpha', sr: 1 }, + { name: 'Vplus', sr: 18 }, + { name: 'Cobra', sr: 12 }, +]; + async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise { const query = { type: ctx.media.type, - title: ctx.media.title, tmdbId: ctx.media.tmdbId, - imdbId: ctx.media.imdbId, ...(ctx.media.type === 'show' && { season: ctx.media.season.number, episode: ctx.media.episode.number, }), - releaseYear: ctx.media.releaseYear, }; return { - embeds: [ - { - embedId: 'vidify-alfa', - url: JSON.stringify(query), - }, - { - embedId: 'vidify-bravo', - url: JSON.stringify(query), - }, - { - embedId: 'vidify-charlie', - url: JSON.stringify(query), - }, - { - embedId: 'vidify-delta', - url: JSON.stringify(query), - }, - { - embedId: 'vidify-echo', - url: JSON.stringify(query), - }, - { - embedId: 'vidify-foxtrot', - url: JSON.stringify(query), - }, - { - embedId: 'vidify-golf', - url: JSON.stringify(query), - }, - { - embedId: 'vidify-hotel', - url: JSON.stringify(query), - }, - { - embedId: 'vidify-india', - url: JSON.stringify(query), - }, - { - embedId: 'vidify-juliett', - url: JSON.stringify(query), - }, - ], + embeds: VIDIFY_SERVERS.map((server) => ({ + embedId: `vidify-${server.name.toLowerCase()}`, + url: JSON.stringify({ ...query, sr: server.sr }), + })), }; } export const vidifyScraper = makeSourcerer({ id: 'vidify', - name: 'Vidify', - rank: 124, + name: 'Vidify 🔥', + rank: 204, disabled: true, - flags: [], + flags: [flags.CORS_ALLOWED], scrapeMovie: comboScraper, scrapeShow: comboScraper, }); diff --git a/src/providers/sources/vidsrc/decrypt.ts b/src/providers/sources/vidsrc/decrypt.ts deleted file mode 100644 index 4897142..0000000 --- a/src/providers/sources/vidsrc/decrypt.ts +++ /dev/null @@ -1,228 +0,0 @@ -const abc = String.fromCharCode( - 65, - 66, - 67, - 68, - 69, - 70, - 71, - 72, - 73, - 74, - 75, - 76, - 77, - 97, - 98, - 99, - 100, - 101, - 102, - 103, - 104, - 105, - 106, - 107, - 108, - 109, - 78, - 79, - 80, - 81, - 82, - 83, - 84, - 85, - 86, - 87, - 88, - 89, - 90, - 110, - 111, - 112, - 113, - 114, - 115, - 116, - 117, - 118, - 119, - 120, - 121, - 122, -); - -const dechar = (x: number): string => String.fromCharCode(x); - -const salt = { - _keyStr: `${abc}0123456789+/=`, - - e(input: string): string { - let t = ''; - let n: number; - let r: number; - let i: number; - let s: number; - let o: number; - let u: number; - let a: number; - let f = 0; - input = salt._ue(input); // eslint-disable-line no-param-reassign - while (f < input.length) { - n = input.charCodeAt(f++); - r = input.charCodeAt(f++); - i = input.charCodeAt(f++); - s = n >> 2; - o = ((n & 3) << 4) | (r >> 4); - u = ((r & 15) << 2) | (i >> 6); - a = i & 63; - - if (Number.isNaN(r)) { - u = 64; - a = 64; - } else if (Number.isNaN(i)) { - a = 64; - } - - t += this._keyStr.charAt(s) + this._keyStr.charAt(o) + this._keyStr.charAt(u) + this._keyStr.charAt(a); - } - return t; - }, - - d(encoded: string): string { - let t = ''; - let n: number; - let r: number; - let i: number; - let s: number; - let o: number; - let u: number; - let a: number; - let f = 0; - - encoded = encoded.replace(/[^A-Za-z0-9+/=]/g, ''); // eslint-disable-line no-param-reassign - while (f < encoded.length) { - s = this._keyStr.indexOf(encoded.charAt(f++)); - o = this._keyStr.indexOf(encoded.charAt(f++)); - u = this._keyStr.indexOf(encoded.charAt(f++)); - a = this._keyStr.indexOf(encoded.charAt(f++)); - - n = (s << 2) | (o >> 4); - r = ((o & 15) << 4) | (u >> 2); - i = ((u & 3) << 6) | a; - - t += dechar(n); - if (u !== 64) t += dechar(r); - if (a !== 64) t += dechar(i); - } - - t = salt._ud(t); - return t; - }, - - _ue(input: string): string { - input = input.replace(/\r\n/g, '\n'); // eslint-disable-line no-param-reassign - let t = ''; - for (let n = 0; n < input.length; n++) { - const r = input.charCodeAt(n); - if (r < 128) { - t += dechar(r); - } else if (r > 127 && r < 2048) { - t += dechar((r >> 6) | 192); - t += dechar((r & 63) | 128); - } else { - t += dechar((r >> 12) | 224); - t += dechar(((r >> 6) & 63) | 128); - t += dechar((r & 63) | 128); - } - } - return t; - }, - - _ud(input: string): string { - let t = ''; - let n = 0; - let r: number; - let c2: number; - let c3: number; - - while (n < input.length) { - r = input.charCodeAt(n); - if (r < 128) { - t += dechar(r); - n++; - } else if (r > 191 && r < 224) { - c2 = input.charCodeAt(n + 1); - t += dechar(((r & 31) << 6) | (c2 & 63)); - n += 2; - } else { - c2 = input.charCodeAt(n + 1); - c3 = input.charCodeAt(n + 2); - t += dechar(((r & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63)); - n += 3; - } - } - return t; - }, -}; - -const sugar = (input: string): string => { - const parts = input.split(dechar(61)); - let result = ''; - const c1 = dechar(120); - - for (const part of parts) { - let encoded = ''; - for (let i = 0; i < part.length; i++) { - encoded += part[i] === c1 ? dechar(49) : dechar(48); - } - const chr = parseInt(encoded, 2); - result += dechar(chr); - } - - return result.substring(0, result.length - 1); -}; - -const pepper = (s: string, n: number): string => { - s = s.replace(/\+/g, '#'); // eslint-disable-line no-param-reassign - s = s.replace(/#/g, '+'); // eslint-disable-line no-param-reassign - - // Default value for vidsrc player - const yValue = 'xx??x?=xx?xx?='; - let a = Number(sugar(yValue)) * n; - if (n < 0) a += abc.length / 2; - const r = abc.substr(a * 2) + abc.substr(0, a * 2); - return s.replace(/[A-Za-z]/g, (c) => r.charAt(abc.indexOf(c))); -}; - -export const decode = (x: string): string => { - if (x.substr(0, 2) === '#1') { - return salt.d(pepper(x.substr(2), -1)); - } - if (x.substr(0, 2) === '#0') { - return salt.d(x.substr(2)); - } - return x; -}; - -export const mirza = (encodedUrl: string, v: any): string => { - let a = encodedUrl.substring(2); - for (let i = 4; i >= 0; i--) { - if (v[`bk${i}`]) { - const b1 = (str: string) => - btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (_, p1) => String.fromCharCode(parseInt(p1, 16)))); - a = a.replace(v.file3_separator + b1(v[`bk${i}`]), ''); - } - } - - const b2 = (str: string) => - decodeURIComponent( - atob(str) - .split('') - .map((c) => `%${`00${c.charCodeAt(0).toString(16)}`.slice(-2)}`) - .join(''), - ); - - return b2(a); -}; diff --git a/src/providers/sources/vidsrc/index.ts b/src/providers/sources/vidsrc/index.ts deleted file mode 100644 index 379dcc9..0000000 --- a/src/providers/sources/vidsrc/index.ts +++ /dev/null @@ -1,124 +0,0 @@ -import type { ShowMedia } from '@/entrypoint/utils/media'; -import { SourcererOutput, makeSourcerer } from '@/providers/base'; -import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; -import { NotFoundError } from '@/utils/errors'; - -import { decode, mirza } from './decrypt'; - -// Default player configuration -const o = { - y: 'xx??x?=xx?xx?=', - u: '#1RyJzl3JYmljm0mkJWOGYWNyI6MfwVNGYXmj9uQj5tQkeYIWoxLCJXNkawOGF5QZ9sQj1YIWowLCJXO20VbVJ1OZ11QGiSlni0QG9uIn19', -}; - -async function vidsrcScrape(ctx: MovieScrapeContext | ShowScrapeContext): Promise { - const imdbId = ctx.media.imdbId; - if (!imdbId) throw new NotFoundError('IMDb ID not found'); - - const isShow = ctx.media.type === 'show'; - let season: number | undefined; - let episode: number | undefined; - - if (isShow) { - const show = ctx.media as ShowMedia; - season = show.season?.number; - episode = show.episode?.number; - } - - const embedUrl = isShow - ? `https://vidsrc.net/embed/tv?imdb=${imdbId}&season=${season}&episode=${episode}` - : `https://vidsrc.net/embed/${imdbId}`; - - ctx.progress(10); - - const embedHtml = await ctx.proxiedFetcher(embedUrl, { - headers: { - Referer: 'https://vidsrc.net/', - 'User-Agent': 'Mozilla/5.0', - }, - }); - - ctx.progress(30); - - // Extract the iframe source using regex - const iframeMatch = embedHtml.match(/]*id="player_iframe"[^>]*src="([^"]*)"[^>]*>/); - if (!iframeMatch) throw new NotFoundError('Initial iframe not found'); - - const rcpUrl = iframeMatch[1].startsWith('//') ? `https:${iframeMatch[1]}` : iframeMatch[1]; - - ctx.progress(50); - - const rcpHtml = await ctx.proxiedFetcher(rcpUrl, { - headers: { Referer: embedUrl, 'User-Agent': 'Mozilla/5.0' }, - }); - - // Find the script with prorcp - const scriptMatch = rcpHtml.match(/src\s*:\s*['"]([^'"]+)['"]/); - if (!scriptMatch) throw new NotFoundError('prorcp iframe not found'); - - const prorcpUrl = scriptMatch[1].startsWith('/') ? `https://cloudnestra.com${scriptMatch[1]}` : scriptMatch[1]; - - ctx.progress(70); - - const finalHtml = await ctx.proxiedFetcher(prorcpUrl, { - headers: { Referer: rcpUrl, 'User-Agent': 'Mozilla/5.0' }, - }); - - // Find script containing Playerjs - const scripts = finalHtml.split(' { }); return parsedCookies; } + +export function getSetCookieHeader(headers: Headers): string { + // Try Set-Cookie first, then x-set-cookie (for proxy scenarios) + return headers.get('Set-Cookie') ?? headers.get('x-set-cookie') ?? ''; +} diff --git a/src/utils/obfuscate.ts b/src/utils/obfuscate.ts new file mode 100644 index 0000000..cc54d9f --- /dev/null +++ b/src/utils/obfuscate.ts @@ -0,0 +1,107 @@ +// Obfuscation utilities for protecting sensitive code and data +// These utilities help protect WASM loading, decryption keys and functions + +// XOR key - this adds another small layer of protection +const KEY = [0x5a, 0xf1, 0x9e, 0x3d, 0x24, 0xb7, 0x6c]; + +/** + * Encodes a string to obfuscate it in the source code + * DO NOT modify this encoding algorithm without updating the decode function + */ +export function encode(str: string): string { + const encoded = []; + for (let i = 0; i < str.length; i++) { + const charCode = str.charCodeAt(i); + const keyChar = KEY[i % KEY.length]; + encoded.push(String.fromCharCode(charCode ^ keyChar)); + } + + // Convert to base64 for better text safety + return btoa(encoded.join('')); +} + +/** + * Decodes a previously encoded string + */ +export function decode(encoded: string): string { + try { + const str = atob(encoded); + const decoded = []; + + for (let i = 0; i < str.length; i++) { + const charCode = str.charCodeAt(i); + const keyChar = KEY[i % KEY.length]; + decoded.push(String.fromCharCode(charCode ^ keyChar)); + } + + return decoded.join(''); + } catch (e) { + // Fallback in case of decoding errors + console.error('Failed to decode string:', e); + return ''; + } +} + +/** + * Decrypt a WASM binary + * This is a placeholder implementation - replace with your actual decryption logic + */ +function decryptWasmBinary(encryptedBuffer: ArrayBuffer, key: Uint8Array): ArrayBuffer { + // Create a view of the encrypted buffer + const encryptedView = new Uint8Array(encryptedBuffer); + const decrypted = new Uint8Array(encryptedBuffer.byteLength); + + // Simple XOR decryption - replace with stronger algorithm in production + for (let i = 0; i < encryptedView.length; i++) { + decrypted[i] = encryptedView[i] ^ key[i % key.length]; + } + + return decrypted.buffer; +} + +/** + * Creates a function from encoded string + * This allows obfuscating function code and only constructing it at runtime + * + * @param encodedFn - The encoded function string (encode(functionString)) + * @returns The decoded and constructed function + */ +export function createFunction any>(encodedFn: string): T { + try { + // Decode the function string + const fnStr = decode(encodedFn); + + // Create the function dynamically + // eslint-disable-next-line no-new-func + return new Function(`return ${fnStr}`)() as T; + } catch (e) { + console.error('Failed to create function from encoded string:', e); + return (() => null) as unknown as T; + } +} + +/** + * Loads and decrypts a WASM module from an encoded URL + * + * @param encodedUrl - The encoded URL to the WASM file + * @param decryptionKey - Key to decrypt the WASM binary (if encrypted) + * @returns A promise resolving to the instantiated WebAssembly instance + */ +export async function loadWasmModule( + encodedUrl: string, + decryptionKey?: Uint8Array, +): Promise { + const url = decode(encodedUrl); + + // Fetch the WASM module + const response = await fetch(url); + let buffer = await response.arrayBuffer(); + + // Decrypt the WASM binary if a key is provided + if (decryptionKey) { + buffer = decryptWasmBinary(buffer, decryptionKey); + } + + // Compile and instantiate the WASM module + return WebAssembly.instantiate(buffer); +} diff --git a/src/utils/proxy.ts b/src/utils/proxy.ts index c8890b1..b9df6a2 100644 --- a/src/utils/proxy.ts +++ b/src/utils/proxy.ts @@ -4,7 +4,7 @@ import { Stream } from '@/providers/streams'; // Default proxy URL for general purpose proxying const DEFAULT_PROXY_URL = 'https://proxy.nsbx.ru/proxy'; // Default M3U8 proxy URL for HLS stream proxying -let CONFIGURED_M3U8_PROXY_URL = 'https://proxy2.pstream.mov'; +let CONFIGURED_M3U8_PROXY_URL = 'https://proxy.pstream.mov'; /** * Set a custom M3U8 proxy URL to use for all M3U8 proxy requests diff --git a/src/utils/turnstile.ts b/src/utils/turnstile.ts new file mode 100644 index 0000000..39923af --- /dev/null +++ b/src/utils/turnstile.ts @@ -0,0 +1,170 @@ +/** + * Cloudflare Turnstile utility for handling invisible CAPTCHA verification + */ + +interface TurnstileConfig { + sitekey: string; + callback?: (token: string) => void; + 'error-callback'?: (error: string) => void; + 'expired-callback'?: () => void; + 'timeout-callback'?: () => void; +} + +declare global { + interface Window { + turnstile?: { + render: (container: string | HTMLElement, config: TurnstileConfig) => string; + reset: (widgetId: string) => void; + remove: (widgetId: string) => void; + getResponse: (widgetId: string) => string; + }; + } +} + +/** + * Loads the Cloudflare Turnstile script if not already loaded + */ +function loadTurnstileScript(): Promise { + return new Promise((resolve, reject) => { + // Check if Turnstile is already loaded + if (window.turnstile) { + resolve(); + return; + } + + // Check if script is already being loaded + if (document.querySelector('script[src*="challenges.cloudflare.com/turnstile"]')) { + // Wait for it to load + const checkLoaded = () => { + if (window.turnstile) { + resolve(); + } else { + setTimeout(checkLoaded, 100); + } + }; + checkLoaded(); + return; + } + + const script = document.createElement('script'); + script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js'; + script.async = true; + script.defer = true; + + script.onload = () => resolve(); + script.onerror = () => reject(new Error('Failed to load Turnstile script')); + + document.head.appendChild(script); + }); +} + +/** + * Creates an invisible Turnstile widget and returns a promise that resolves with the token + * @param sitekey The Turnstile site key + * @param timeout Optional timeout in milliseconds (default: 30000) + * @returns Promise that resolves with the Turnstile token + */ +export async function getTurnstileToken(sitekey: string, timeout: number = 30000): Promise { + // Only run in browser environment + if (typeof window === 'undefined') { + throw new Error('Turnstile verification requires browser environment'); + } + + try { + // Load Turnstile script + await loadTurnstileScript(); + + // Create a hidden container for the Turnstile widget + const container = document.createElement('div'); + container.style.position = 'absolute'; + container.style.left = '-9999px'; + container.style.top = '-9999px'; + container.style.width = '1px'; + container.style.height = '1px'; + container.style.overflow = 'hidden'; + container.style.opacity = '0'; + container.style.pointerEvents = 'none'; + + document.body.appendChild(container); + + return new Promise((resolve, reject) => { + let widgetId: string; + let timeoutId: NodeJS.Timeout; + + const cleanup = () => { + if (timeoutId) clearTimeout(timeoutId); + if (widgetId && window.turnstile) { + try { + window.turnstile.remove(widgetId); + } catch (e) { + // Ignore errors during cleanup + } + } + if (container.parentNode) { + container.parentNode.removeChild(container); + } + }; + + // Set up timeout + timeoutId = setTimeout(() => { + cleanup(); + reject(new Error('Turnstile verification timed out')); + }, timeout); + + try { + // Render the Turnstile widget + widgetId = window.turnstile!.render(container, { + sitekey, + callback: (token: string) => { + cleanup(); + resolve(token); + }, + 'error-callback': (error: string) => { + cleanup(); + reject(new Error(`Turnstile error: ${error}`)); + }, + 'expired-callback': () => { + cleanup(); + reject(new Error('Turnstile token expired')); + }, + 'timeout-callback': () => { + cleanup(); + reject(new Error('Turnstile verification timed out')); + }, + }); + } catch (error) { + cleanup(); + reject(new Error(`Failed to render Turnstile widget: ${error}`)); + } + }); + } catch (error) { + throw new Error(`Turnstile verification failed: ${error}`); + } +} + +/** + * Validates a Turnstile token by making a request to Cloudflare's verification endpoint + * @param token The Turnstile token to validate + * @param secret The Turnstile secret key (server-side only) + * @returns Promise that resolves with validation result + */ +export async function validateTurnstileToken(token: string, secret: string): Promise { + try { + const response = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + secret, + response: token, + }), + }); + + const result = await response.json(); + return result.success === true; + } catch (error) { + console.error('Turnstile validation error:', error); + return false; + } +}