diff --git a/package.json b/package.json index a5de0ec..2479d91 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "@hansalle/providers", + "name": "@p-stream/providers", "version": "3.2.0", - "description": "Package that contains all the providers of hansalle", + "description": "Package that contains all the providers of p-stream", "type": "module", "main": "./lib/index.js", "types": "./lib/index.d.ts", @@ -72,6 +72,7 @@ "eslint-plugin-prettier": "^5.5.4", "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", @@ -87,7 +88,6 @@ "abort-controller": "^3.0.0", "cheerio": "1.0.0-rc.12", "cookie": "^0.6.0", - "puppeteer": "*", "crypto-js": "^4.2.0", "form-data": "^4.0.4", "fuse.js": "^7.1.0", @@ -100,4 +100,4 @@ "unpacker": "^1.0.1" }, "packageManager": "pnpm@9.14.4" -} +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e939768..6bd14f2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,9 +41,6 @@ importers: node-fetch: specifier: ^3.3.2 version: 3.3.2 - puppeteer: - specifier: '*' - version: 24.39.1(typescript@5.9.3) set-cookie-parser: specifier: ^2.7.1 version: 2.7.1 @@ -114,6 +111,9 @@ importers: prettier: specifier: ^3.6.2 version: 3.6.2 + 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) @@ -471,8 +471,8 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@puppeteer/browsers@2.13.0': - resolution: {integrity: sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==} + '@puppeteer/browsers@2.3.0': + resolution: {integrity: sha512-ioXoq9gPxkss4MYhD+SFaU9p1IHFUX0ILAWFPyjGaBdjLsYAlZw6j1iLA0N/m12uVHLFDfSYNF7EQccjinIMDA==} engines: {node: '>=18'} hasBin: true @@ -1148,8 +1148,8 @@ packages: chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} - chromium-bidi@14.0.0: - resolution: {integrity: sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==} + chromium-bidi@0.6.3: + resolution: {integrity: sha512-qXlsCmpCZJAnoTYI83Iu6EdYQpMYdVkCfq08KDh2pmlVqK5t5IA9mGs4/LwCwp4fqisSOMXZxP3HIh8w8aRn0A==} peerDependencies: devtools-protocol: '*' @@ -1333,8 +1333,8 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - devtools-protocol@0.0.1581282: - resolution: {integrity: sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==} + devtools-protocol@0.0.1312386: + resolution: {integrity: sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA==} diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} @@ -2497,13 +2497,14 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - puppeteer-core@24.39.1: - resolution: {integrity: sha512-AMqQIKoEhPS6CilDzw0Gd1brLri3emkC+1N2J6ZCCuY1Cglo56M63S0jOeBZDQlemOiRd686MYVMl9ELJBzN3A==} + puppeteer-core@22.15.0: + resolution: {integrity: sha512-cHArnywCiAAVXa3t4GGL2vttNxh7GqXtIYGym99egkNJ3oG//wL9LkvO4WE8W1TJe95t1F1ocu9X4xWaGsOKOA==} engines: {node: '>=18'} - puppeteer@24.39.1: - resolution: {integrity: sha512-68Zc9QpcVvfxp2C+3UL88TyUogEAn5tSylXidbEuEXvhiqK1+v65zeBU5ubinAgEHMGr3dcSYqvYrGtdzsPI3w==} + puppeteer@22.15.0: + resolution: {integrity: sha512-XjCY1SiSEi1T7iSYuxS82ft85kwDJUS7wj1Z0eGVXKdtr5g4xnVcbjwxhq5xBnpK/E7x1VZZoJDxpjAOasHT4Q==} engines: {node: '>=18'} + deprecated: < 24.15.0 is no longer supported hasBin: true queue-lit@1.5.2: @@ -2620,11 +2621,6 @@ packages: engines: {node: '>=10'} hasBin: true - semver@7.7.3: - resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} - engines: {node: '>=10'} - hasBin: true - semver@7.7.4: resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} engines: {node: '>=10'} @@ -2844,6 +2840,9 @@ packages: text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -2937,9 +2936,6 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} - typed-query-selector@2.12.1: - resolution: {integrity: sha512-uzR+FzI8qrUEIu96oaeBJmd9E7CFEiQ3goA5qCVgc4s5llSubcfGHq9yUstZx/k4s9dXHVKsE35YWoFyvEqEHA==} - typescript@5.4.2: resolution: {integrity: sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==} engines: {node: '>=14.17'} @@ -2957,6 +2953,9 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} + unbzip2-stream@1.4.3: + resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==} + undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} @@ -2973,6 +2972,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + urlpattern-polyfill@10.0.0: + resolution: {integrity: sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -3071,9 +3073,6 @@ packages: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} - webdriver-bidi-protocol@0.4.1: - resolution: {integrity: sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==} - webidl-conversions@8.0.0: resolution: {integrity: sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==} engines: {node: '>=20'} @@ -3128,18 +3127,6 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@8.18.3: - resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - ws@8.19.0: resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} engines: {node: '>=10.0.0'} @@ -3190,8 +3177,8 @@ packages: engines: {node: '>=8.0.0'} hasBin: true - zod@3.25.76: - resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@3.23.8: + resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} snapshots: @@ -3497,7 +3484,7 @@ snapshots: '@pkgr/core@0.2.9': {} - '@puppeteer/browsers@2.13.0': + '@puppeteer/browsers@2.3.0': dependencies: debug: 4.4.3 extract-zip: 2.0.1 @@ -3505,6 +3492,7 @@ snapshots: proxy-agent: 6.5.0 semver: 7.7.4 tar-fs: 3.1.2 + unbzip2-stream: 1.4.3 yargs: 17.7.2 transitivePeerDependencies: - bare-abort-controller @@ -3732,7 +3720,7 @@ snapshots: globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 - semver: 7.7.3 + semver: 7.7.4 ts-api-utils: 1.4.3(typescript@5.9.3) optionalDependencies: typescript: 5.9.3 @@ -4072,8 +4060,7 @@ snapshots: dependencies: bare-path: 3.0.0 - base64-js@1.5.1: - optional: true + base64-js@1.5.1: {} basic-ftp@5.2.0: {} @@ -4114,7 +4101,6 @@ snapshots: dependencies: base64-js: 1.5.1 ieee754: 1.2.1 - optional: true cac@6.7.14: {} @@ -4208,11 +4194,12 @@ snapshots: chownr@1.1.4: optional: true - chromium-bidi@14.0.0(devtools-protocol@0.0.1581282): + chromium-bidi@0.6.3(devtools-protocol@0.0.1312386): dependencies: - devtools-protocol: 0.0.1581282 + devtools-protocol: 0.0.1312386 mitt: 3.0.1 - zod: 3.25.76 + urlpattern-polyfill: 10.0.0 + zod: 3.23.8 class-validator@0.14.1: dependencies: @@ -4388,7 +4375,7 @@ snapshots: detect-libc@2.1.2: optional: true - devtools-protocol@0.0.1581282: {} + devtools-protocol@0.0.1312386: {} diff-sequences@29.6.3: {} @@ -5033,8 +5020,7 @@ snapshots: safer-buffer: 2.1.2 optional: true - ieee754@1.2.1: - optional: true + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -5105,7 +5091,7 @@ snapshots: is-bun-module@2.0.0: dependencies: - semver: 7.7.3 + semver: 7.7.4 is-callable@1.2.7: {} @@ -5291,7 +5277,7 @@ snapshots: whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 whatwg-url: 15.1.0 - ws: 8.18.3 + ws: 8.19.0 xml-name-validator: 5.0.0 optionalDependencies: canvas: 3.2.0 @@ -5381,7 +5367,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.7.3 + semver: 7.7.4 math-intrinsics@1.1.0: {} @@ -5471,7 +5457,7 @@ snapshots: node-abi@3.80.0: dependencies: - semver: 7.7.3 + semver: 7.7.4 optional: true node-addon-api@7.1.1: @@ -5737,14 +5723,12 @@ snapshots: punycode@2.3.1: {} - puppeteer-core@24.39.1: + puppeteer-core@22.15.0: dependencies: - '@puppeteer/browsers': 2.13.0 - chromium-bidi: 14.0.0(devtools-protocol@0.0.1581282) + '@puppeteer/browsers': 2.3.0 + chromium-bidi: 0.6.3(devtools-protocol@0.0.1312386) debug: 4.4.3 - devtools-protocol: 0.0.1581282 - typed-query-selector: 2.12.1 - webdriver-bidi-protocol: 0.4.1 + devtools-protocol: 0.0.1312386 ws: 8.19.0 transitivePeerDependencies: - bare-abort-controller @@ -5754,14 +5738,12 @@ snapshots: - supports-color - utf-8-validate - puppeteer@24.39.1(typescript@5.9.3): + puppeteer@22.15.0(typescript@5.9.3): dependencies: - '@puppeteer/browsers': 2.13.0 - chromium-bidi: 14.0.0(devtools-protocol@0.0.1581282) + '@puppeteer/browsers': 2.3.0 cosmiconfig: 9.0.1(typescript@5.9.3) - devtools-protocol: 0.0.1581282 - puppeteer-core: 24.39.1 - typed-query-selector: 2.12.1 + devtools-protocol: 0.0.1312386 + puppeteer-core: 22.15.0 transitivePeerDependencies: - bare-abort-controller - bare-buffer @@ -5923,8 +5905,6 @@ snapshots: dependencies: lru-cache: 6.0.0 - semver@7.7.3: {} - semver@7.7.4: {} set-cookie-parser@2.7.1: {} @@ -6207,6 +6187,8 @@ snapshots: text-table@0.2.0: {} + through@2.3.8: {} + tinybench@2.9.0: {} tinyglobby@0.2.15: @@ -6321,8 +6303,6 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typed-query-selector@2.12.1: {} - typescript@5.4.2: {} typescript@5.9.3: {} @@ -6336,6 +6316,11 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 + unbzip2-stream@1.4.3: + dependencies: + buffer: 5.7.1 + through: 2.3.8 + undici-types@7.16.0: {} universalify@0.1.2: {} @@ -6370,6 +6355,8 @@ snapshots: dependencies: punycode: 2.3.1 + urlpattern-polyfill@10.0.0: {} + util-deprecate@1.0.2: optional: true @@ -6472,7 +6459,7 @@ snapshots: dependencies: '@volar/typescript': 1.11.1 '@vue/language-core': 1.8.27(typescript@5.9.3) - semver: 7.7.3 + semver: 7.7.4 typescript: 5.9.3 w3c-xmlserializer@5.0.0: @@ -6482,8 +6469,6 @@ snapshots: web-streams-polyfill@3.3.3: {} - webdriver-bidi-protocol@0.4.1: {} - webidl-conversions@8.0.0: optional: true @@ -6561,9 +6546,6 @@ snapshots: wrappy@1.0.2: {} - ws@8.18.3: - optional: true - ws@8.19.0: {} xml-name-validator@5.0.0: @@ -6605,4 +6587,4 @@ snapshots: optionalDependencies: commander: 9.5.0 - zod@3.25.76: {} + zod@3.23.8: {} diff --git a/src/providers/sources/florence/bludclart.ts b/src/providers/sources/florence/bludclart.ts index a38cde1..cd1a401 100644 --- a/src/providers/sources/florence/bludclart.ts +++ b/src/providers/sources/florence/bludclart.ts @@ -5,12 +5,7 @@ import { SourcererOutput, makeSourcerer } from '@/providers/base'; import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; import { NotFoundError } from '@/utils/errors'; -const SHOWBOX_BASE = 'https://www.showbox.media'; -const FEBBOX_BASE = 'https://www.febbox.com'; -const MEDIA_PROXY_API = 'https://media-proxy.oct-cdn.co/api/fetchMp4'; - const PROXY_URLS = [ - 'https://simple-proxy.asteral-ss2.workers.dev', 'https://pcors.shipwr3ck.workers.dev', 'https://pstream-proxy.katelyn-boreham.workers.dev', 'https://simple-proxy-pstream.mohamdaimn.workers.dev', @@ -31,6 +26,7 @@ const PROXY_URLS = [ 'https://simple-proxy.quantality.workers.dev', 'https://simple-proxy.ratshrabies.workers.dev', 'https://simple-proxy.remuxbay.workers.dev', + 'https://simple-proxy.six666666666666666666-6-6-6-6-6-6-6-6-6-6.workers.dev', 'https://simple-proxy.spencerjamesdeal.workers.dev', 'https://simple-proxy.starseekerdude.workers.dev', 'https://simple-proxy.subhashgottumukkala17.workers.dev', @@ -39,15 +35,25 @@ const PROXY_URLS = [ 'https://simple-proxy.valdezmarcjoshua.workers.dev', 'https://simple-proxy.ytonlyamit.workers.dev', 'https://simple-proxy2.tonyvu4913.workers.dev', + 'https://simple-proxyyy.thinner-life-void.workers.dev', 'https://bruh.jerry5890.workers.dev', 'https://c719dda0-simple-proxy.sylaxx95.workers.dev', ]; +const SHOWBOX_BASE = 'https://www.showbox.media'; +const FEBBOX_BASE = 'https://www.febbox.com'; +const MEDIA_PROXY_API = 'https://media-proxy.oct-cdn.co/api/fetchMp4'; + const REQUEST_HEADERS = { - 'user-agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', - accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', + 'user-agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0', + accept: 'application/json, text/plain, */*', 'accept-language': 'en-US,en;q=0.5', + 'accept-encoding': 'gzip, deflate, br, zstd', + origin: 'https://watch.bludclart.com', + referer: 'https://watch.bludclart.com/', + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'cross-site', }; function proxyUrl(url: string): string { @@ -55,120 +61,327 @@ function proxyUrl(url: string): string { return `${randomProxy}/?destination=${encodeURIComponent(url)}`; } -async function customFetcher(ctx: any, url: string, options?: any): Promise { - const proxiedUrl = proxyUrl(url); - const res = await ctx.fetcher(proxiedUrl, options); - if (typeof res === 'string' && (res.trim().startsWith('{') || res.trim().startsWith('['))) { +async function customFetcher(ctx: ShowScrapeContext | MovieScrapeContext, url: string, options?: any): Promise { + // Try up to 3 different proxies before falling back to direct access + for (let attempt = 0; attempt < 3; attempt++) { try { - return JSON.parse(res); - } catch { - return res; + const proxiedUrl = proxyUrl(url); + return await ctx.fetcher(proxiedUrl, options); + } catch (error) { + // If it's a 520 error (Cloudflare issue), try next proxy + if (attempt < 2) continue; + // On last proxy attempt failure, try direct access + return ctx.proxiedFetcher(url, options); } } - return res; } -async function getShowboxId(ctx: any, query: string): Promise<{ id: string; slug: string } | undefined> { +async function getShareKey( + ctx: ShowScrapeContext | MovieScrapeContext, + showboxId: string, + mediaType: string, +): Promise { + const typeCode = mediaType === 'tv' ? '2' : '1'; + const shareLinkUrl = `${SHOWBOX_BASE}/index/share_link?id=${showboxId}&type=${typeCode}`; + + try { + const response = await customFetcher(ctx, shareLinkUrl, { + headers: REQUEST_HEADERS, + }); + + const data = response; + + if (typeof data === 'object' && data !== null) { + // Check for nested data + if ('data' in data && typeof data.data === 'object' && data.data !== null && 'link' in data.data) { + return (data.data as any).link.split('/').pop(); + } + + if ('link' in data) { + // Maybe it returns a febbox link? https://www.febbox.com/share/KEY + return (data as any).link.split('/').pop(); + } + if ('key' in data) { + return (data as any).key; + } + if ('data' in data) { + // generic data field + const val = (data as any).data; + if (typeof val === 'string' && val.includes('febbox.com')) { + return val.split('/').pop() || null; + } + return String(val); + } + } + + return String(data); + } catch (error) { + return null; + } +} + +async function getShowboxId( + ctx: ShowScrapeContext | MovieScrapeContext, + query: string, + _mediaType: string, +): Promise { const searchUrl = `${SHOWBOX_BASE}/search?keyword=${encodeURIComponent(query)}`; - for (let i = 0; i < 3; i++) { + try { const response = await customFetcher(ctx, searchUrl, { - headers: { ...REQUEST_HEADERS, referer: `${SHOWBOX_BASE}/` }, + headers: REQUEST_HEADERS, }); - if (!response || typeof response !== 'string') continue; const $ = load(response); - const el = $('.film-poster').first(); - let id = el.find('.film-poster-ahref').attr('data-id') || el.attr('data-movie-id'); - const href = el.find('.film-poster-ahref').attr('href') || el.find('a').attr('href'); - const slug = href ? href.split('/').pop() : undefined; - if (!id && slug?.includes('-')) id = slug.split('-').pop(); + const results = $('.film-poster-ahref'); - if (id && slug) return { id, slug }; + if (results.length === 0) { + return null; + } + + let targetUrl: string | null = null; + for (const result of results.toArray()) { + const title = $(result).attr('title')?.trim(); + const href = $(result).attr('href'); + + if (title?.toLowerCase() === query.toLowerCase()) { + targetUrl = `${SHOWBOX_BASE}${href}`; + break; + } + } + + if (!targetUrl && results.length > 0) { + const href = $(results[0]).attr('href'); + targetUrl = `${SHOWBOX_BASE}${href}`; + } + + if (targetUrl) { + const detailResponse = await customFetcher(ctx, targetUrl, { + headers: REQUEST_HEADERS, + }); + + const detail$ = load(detailResponse); + const headingLink = detail$('h2.heading-name a'); + + if (headingLink.length > 0) { + const href = headingLink.attr('href'); + const showId = href?.split('/').pop(); + return showId || null; + } + + // Fallback: extract from URL + const match = targetUrl.match(/-(\d+)$/); + if (match) { + return match[1]; + } + } + + return null; + } catch (error) { + return null; } - return undefined; } -async function getShareKey(ctx: any, id: string, slug: string, type: string): Promise { - const typeCode = type === 'tv' ? '2' : '1'; - const shareLinkUrl = `${SHOWBOX_BASE}/index/share_link?id=${id}&type=${typeCode}`; - const referer = `${SHOWBOX_BASE}/${type === 'tv' ? 'tv' : 'movie'}/${slug}`; +async function getFebboxFileList( + ctx: ShowScrapeContext | MovieScrapeContext, + shareKey: string, + parentId: number = 0, +): Promise { + const pidStr = parentId.toString(); + const listUrl = `${FEBBOX_BASE}/file/file_share_list?share_key=${shareKey}&pwd=&parent_id=${pidStr}`; - const response = await customFetcher(ctx, shareLinkUrl, { - headers: { ...REQUEST_HEADERS, 'x-requested-with': 'XMLHttpRequest', referer }, + const response = await customFetcher(ctx, listUrl, { + headers: REQUEST_HEADERS, }); - const link = - response?.data?.link || - response?.link || - (typeof response?.data === 'string' && response.data.includes('febbox') ? response.data : undefined); - return link && typeof link === 'string' ? link.split('/').pop() : undefined; + return response; } -async function findFileFid(ctx: any, shareKey: string, media: any): Promise { - const isShow = media.type === 'show'; - const listUrl = `${FEBBOX_BASE}/file/file_share_list?share_key=${shareKey}&pwd=&parent_id=0`; - const rootData = await customFetcher(ctx, listUrl, { headers: REQUEST_HEADERS }); - let files = rootData?.data?.file_list || []; - - if (isShow) { - const sNum = media.season.number; - const seasonFolder = files.find( - (f: any) => f.is_dir === 1 && f.file_name.toLowerCase().replace(/\s/g, '').includes(`season${sNum}`), - ); - if (!seasonFolder) return undefined; - - const seasonUrl = `${FEBBOX_BASE}/file/file_share_list?share_key=${shareKey}&pwd=&parent_id=${seasonFolder.fid}`; - const seasonData = await customFetcher(ctx, seasonUrl, { headers: REQUEST_HEADERS }); - files = seasonData?.data?.file_list || []; - - const epStr = `E${media.episode.number.toString().padStart(2, '0')}`; - const episodeFile = files.find((f: any) => f.is_dir === 0 && f.file_name.toUpperCase().includes(epStr)); - return episodeFile?.fid; +async function findFileFid( + ctx: ShowScrapeContext | MovieScrapeContext, + shareKey: string, + mediaType: string, + season?: number, + episode?: number, +): Promise { + const rootData = await getFebboxFileList(ctx, shareKey); + if (!rootData || !rootData.data || !rootData.data.file_list) { + return null; } - const video = files - .filter((f: any) => f.is_dir === 0) - .sort((a: any, b: any) => (b.file_size_bytes || 0) - (a.file_size_bytes || 0))[0]; - return video?.fid; + const files = rootData.data.file_list; + + if (mediaType === 'movie') { + const videoFiles = files.filter((f: any) => f.ext && ['mp4', 'mkv', 'avi'].includes(f.ext.toLowerCase())); + if (videoFiles.length === 0) { + return null; + } + + videoFiles.sort( + (a: any, b: any) => parseInt(b.file_size_bytes || '0', 10) - parseInt(a.file_size_bytes || '0', 10), + ); + return videoFiles[0].fid; + } + + if (mediaType === 'tv' && season && episode) { + const seasonFolderName = `Season ${season}`; + const seasonFolderNamePad = `Season ${season.toString().padStart(2, '0')}`; + + let seasonFid: number | null = null; + for (const f of files) { + if (f.is_dir === 1) { + const fname = f.file_name.trim(); + if ( + fname.toLowerCase() === seasonFolderName.toLowerCase() || + fname.toLowerCase() === seasonFolderNamePad.toLowerCase() + ) { + seasonFid = f.fid; + break; + } + } + } + + if (!seasonFid) { + for (const f of files) { + if (f.is_dir === 1) { + const fname = f.file_name.trim(); + const seasonRegex = new RegExp(`S0?${season}\\b`, 'i'); + const seasonOnlyRegex = new RegExp(`\\b${season}\\b.*season`, 'i'); + if (seasonRegex.test(fname) && !seasonOnlyRegex.test(fname)) { + seasonFid = f.fid; + break; + } + } + } + } + + if (!seasonFid) { + return null; + } + + const seasonData = await getFebboxFileList(ctx, shareKey, seasonFid); + if (!seasonData || !seasonData.data || !seasonData.data.file_list) { + return null; + } + + const episodeFiles = seasonData.data.file_list; + + const episodePatterns = [ + new RegExp(`S0?${season}E0?${episode}\\b`, 'i'), + new RegExp(`${season}x0?${episode}\\b`, 'i'), + new RegExp(`E0?${episode}\\b`, 'i'), + new RegExp(`\\b${episode}\\b`, 'i'), // Risky, might match other numbers + ]; + + for (const f of episodeFiles) { + if (f.is_dir === 0 && f.ext && ['mp4', 'mkv', 'avi'].includes(f.ext.toLowerCase())) { + const fname = f.file_name; + for (const pattern of episodePatterns) { + if (pattern.test(fname)) { + return f.fid; + } + } + } + } + } + + return null; +} + +async function getStreamUrl( + ctx: ShowScrapeContext | MovieScrapeContext, + fid: number, + shareKey: string, +): Promise { + const payload = { + fid, + share_key: shareKey, + user_token: null, + }; + + try { + const response = await customFetcher(ctx, MEDIA_PROXY_API, { + method: 'POST', + headers: { + ...REQUEST_HEADERS, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }); + + if (response && response.sources && response.sources.length > 0) { + const sources = response.sources; + + // Sort by quality ranking + const qualityRank = (q: string): number => { + const quality = q.toUpperCase(); + if (quality.includes('1080')) return 100; + if (quality.includes('720')) return 80; + if (quality === 'ORG') return 60; + if (quality.includes('480')) return 40; + if (quality.includes('360')) return 20; + return 0; + }; + + sources.sort((a: any, b: any) => qualityRank(b.quality) - qualityRank(a.quality)); + return sources[0].download_url; + } + + return null; + } catch (error) { + return null; + } } async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise { - const mediaType = ctx.media.type === 'show' ? 'tv' : 'movie'; + ctx.progress(10); - // 1. Get IDs (Using nullish coalescing to satisfy TS) - const result = await getShowboxId(ctx, ctx.media.title); - if (!result || !result.id || !result.slug) throw new NotFoundError('No Showbox ID found'); + const isShow = ctx.media.type === 'show'; + const mediaType = isShow ? 'tv' : 'movie'; + const queryName = (ctx.media.title || '').replace('and', '&'); - // 2. Get Share Key - const shareKey = await getShareKey(ctx, result.id, result.slug, mediaType); - if (!shareKey) throw new NotFoundError('No Share Key found'); + const showboxId = await getShowboxId(ctx, queryName, mediaType); + if (!showboxId) { + throw new NotFoundError('Could not find Showbox ID'); + } - // 3. Find FID - const fid = await findFileFid(ctx, shareKey, ctx.media); - if (!fid) throw new NotFoundError('No File ID found'); + const shareKey = await getShareKey(ctx, showboxId, mediaType); + if (!shareKey) { + throw new NotFoundError('Could not get Share Key'); + } - // 4. Resolve Stream - const response = await customFetcher(ctx, MEDIA_PROXY_API, { - method: 'POST', - headers: { ...REQUEST_HEADERS, 'content-type': 'application/json' }, - body: JSON.stringify({ fid, share_key: shareKey }), - }); + let fid: number | null; + if (isShow) { + const showCtx = ctx as ShowScrapeContext; + fid = await findFileFid(ctx, shareKey, mediaType, showCtx.media.season.number, showCtx.media.episode.number); + } else { + fid = await findFileFid(ctx, shareKey, mediaType); + } - const source = response?.sources?.find((s: any) => s.download_url.includes('.m3u8')); - if (!source) throw new NotFoundError('No stream found'); + if (!fid) { + throw new NotFoundError('Could not find file ID'); + } + + ctx.progress(50); + + const streamUrl = await getStreamUrl(ctx, fid, shareKey); + + if (!streamUrl) { + throw new NotFoundError('No stream found'); + } + + ctx.progress(90); return { embeds: [], stream: [ { id: 'primary', - type: 'hls', - playlist: source.download_url, + type: 'hls' as const, + playlist: streamUrl, flags: [flags.CORS_ALLOWED], captions: [], - headers: { Referer: FEBBOX_BASE }, }, ], }; @@ -176,7 +389,7 @@ async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promis export const bludclartScraper = makeSourcerer({ id: 'bludclart', - name: 'Bludclart 🤝', + name: 'Bludclart', rank: 202, disabled: false, flags: [flags.CORS_ALLOWED], diff --git a/vite.config.ts b/vite.config.ts index 0534234..82610ca 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -28,9 +28,16 @@ export default defineConfig((env) => ({ build: { minify: false, rollupOptions: { - external: Object.keys(pkg.dependencies), + external: [ + ...Object.keys(pkg.dependencies), + 'puppeteer', + '@puppeteer/browsers', + 'proxy-agent' + ], output: { - globals: Object.fromEntries(Object.keys(pkg.dependencies).map((v) => [v, v])), + globals: Object.fromEntries( + Object.keys(pkg.dependencies).map((v) => [v, v]) + ), }, }, outDir: 'lib',