From ce1288490d07d086f359fa4ee449064ab6b7872b Mon Sep 17 00:00:00 2001 From: Inside4ndroid Date: Fri, 4 Apr 2025 23:36:41 +0100 Subject: [PATCH] Major Update : Added 2Embed, Update api routes, update json response format. --- README.md | 143 +++++++++++++++- index.js | 63 ++++++- package-lock.json | 279 +++++++++++++++++++++++++++++++ package.json | 1 + src/extractors/2Embed.js | 340 ++++++++++++++++++++++++++++++++++++++ src/extractors/embedsu.js | 169 +++++++++++++------ src/utils/jsunpack.js | 106 ++++++++++++ 7 files changed, 1039 insertions(+), 62 deletions(-) create mode 100644 src/extractors/2Embed.js create mode 100644 src/utils/jsunpack.js diff --git a/README.md b/README.md index 0f0d08b..30ee767 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,20 @@ -# Unoffical vidsrc (embed.su) API! 👋 +# Unoffical vidsrc API! 👋 +## Current Embed Providers: + +1. embedsu +2. 2embed + +
+ +Hello! I'm Inside4ndroid, a passionate Software Developer. + +- 🐛 Bugs will be found and squashed as soon as possible please report any isues. +- 🌱 Currently in a state that works and is recieving updates. + +--- + +
+ ## Sponsorship / Donations Your sponsorship is vital in helping me achieve this mission. With your support, I can: @@ -12,9 +28,11 @@ Every contribution, no matter the size, makes a significant impact. Thank you for considering supporting my work! -### NOTE: +
-This is an api for id's provided at [TMDB](https://www.themoviedb.org/) +### USAGE: + +This is an api for id's provided at [TMDB](https://www.themoviedb.org/). you must use the ID from the tmdb url like this : @@ -45,6 +63,8 @@ The providers are currently : ``` embedsu +2embed +combined (use this as provider to return results for all providers) ``` ## Example Movie @@ -57,6 +77,115 @@ https://your-domain/embedsu/916224 https://your-domain/embedsu/1429?s=1&e=1 ``` +## Example JSON Response + +
+ Click me + +```Json +{ + "embedsu": { + "sources": [ + { + "provider": "EmbedSu", + "files": [ + { + "file": "https://embed.su/api/proxy/viper/mistydawn62.pro/file2/dCjXQHTrXL5VWmkFvGEEIrCyskIr6MZERXLYef0sUKuY12Ey+dX5hLYObC78qdmoop2iV8+MYm5wcOiFtAuZbcsp46BsOa1q5in7CNuEv70H5ZNZ34kEPH5I9FpN3v6qN7WhRBXq0ThSHefHVb94rMFoirhHjC4o2LRkwXoAiTA=/MTA4MA==/aW5kZXgubTN1OA==.m3u8", + "type": "hls", + "quality": "1080p", + "lang": "en" + }, + { + "file": "https://embed.su/api/proxy/viper/mistydawn62.pro/file2/dCjXQHTrXL5VWmkFvGEEIrCyskIr6MZERXLYef0sUKuY12Ey+dX5hLYObC78qdmoop2iV8+MYm5wcOiFtAuZbcsp46BsOa1q5in7CNuEv70H5ZNZ34kEPH5I9FpN3v6qN7WhRBXq0ThSHefHVb94rMFoirhHjC4o2LRkwXoAiTA=/NzIw/aW5kZXgubTN1OA==.m3u8", + "type": "hls", + "quality": "720p", + "lang": "en" + }, + { + "file": "https://embed.su/api/proxy/viper/mistydawn62.pro/file2/dCjXQHTrXL5VWmkFvGEEIrCyskIr6MZERXLYef0sUKuY12Ey+dX5hLYObC78qdmoop2iV8+MYm5wcOiFtAuZbcsp46BsOa1q5in7CNuEv70H5ZNZ34kEPH5I9FpN3v6qN7WhRBXq0ThSHefHVb94rMFoirhHjC4o2LRkwXoAiTA=/MzYw/aW5kZXgubTN1OA==.m3u8", + "type": "hls", + "quality": "360p", + "lang": "en" + } + ], + "subtitles": [ + { + "url": "https://cca.megafiles.store/1c/9e/1c9eb9aeb90915a2786400a748c8b6a4/eng-2.vtt", + "lang": "English" + }, + { + "url": "https://cca.megafiles.store/1c/9e/1c9eb9aeb90915a2786400a748c8b6a4/eng-3.vtt", + "lang": "English" + }, + { + "url": "https://cca.megafiles.store/1c/9e/1c9eb9aeb90915a2786400a748c8b6a4/fre-4.vtt", + "lang": "French" + }, + { + "url": "https://cca.megafiles.store/1c/9e/1c9eb9aeb90915a2786400a748c8b6a4/spa-5.vtt", + "lang": "Spanish" + } + ], + "headers": { + "Referer": "https://embed.su", + "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", + "Origin": "https://embed.su" + } + } + ] + }, + "twoembed": { + "sources": { + "sources": [ + { + "provider": "2Embed/Swish", + "files": [ + { + "file": "https://j7fqjm25xt.premilkyway.com/hls2/01/05031/6z843r5do14n_,l,n,h,.urlset/index-f3-v1-a1.m3u8?t=T98eEbm347MS_kxTv3tPxGucVi-ZnV6omagdGNf-ZiA&s=1743805861&e=129600&f=25158304&srv=3FjgDmcnea8S35m&i=0.4&sp=500&p1=3FjgDmcnea8S35m&p2=3FjgDmcnea8S35m&asn=5378", + "type": "hls", + "quality": "1080p", + "lang": "en" + }, + { + "file": "https://j7fqjm25xt.premilkyway.com/hls2/01/05031/6z843r5do14n_,l,n,h,.urlset/index-f2-v1-a1.m3u8?t=T98eEbm347MS_kxTv3tPxGucVi-ZnV6omagdGNf-ZiA&s=1743805861&e=129600&f=25158304&srv=3FjgDmcnea8S35m&i=0.4&sp=500&p1=3FjgDmcnea8S35m&p2=3FjgDmcnea8S35m&asn=5378", + "type": "hls", + "quality": "720p", + "lang": "en" + }, + { + "file": "https://j7fqjm25xt.premilkyway.com/hls2/01/05031/6z843r5do14n_,l,n,h,.urlset/index-f1-v1-a1.m3u8?t=T98eEbm347MS_kxTv3tPxGucVi-ZnV6omagdGNf-ZiA&s=1743805861&e=129600&f=25158304&srv=3FjgDmcnea8S35m&i=0.4&sp=500&p1=3FjgDmcnea8S35m&p2=3FjgDmcnea8S35m&asn=5378", + "type": "hls", + "quality": "480p", + "lang": "en" + } + ], + "subtitles": [ + { + "url": "https://3FjgDmcnea8S35m.premilkyway.com/vtt/01/05031/6z843r5do14n_fre.vtt", + "lang": "French" + }, + { + "url": "https://3FjgDmcnea8S35m.premilkyway.com/vtt/01/05031/6z843r5do14n_spa.vtt", + "lang": "Argentina" + }, + { + "url": "https://3FjgDmcnea8S35m.premilkyway.com/vtt/01/05031/6z843r5do14n_eng.vtt", + "lang": "English" + } + ], + "headers": { + "Referer": "https://uqloads.xyz", + "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", + "Origin": "https://uqloads.xyz" + } + } + ] + } + } +} +``` +
+ ## Deployment You can self host by doing the following : @@ -67,3 +196,11 @@ You can self host by doing the following : 4. `npm run main` "This no longer works on vercel due to the source blocking vercel requests but will work on your own server self hosted." + +## WARNING + +Using this without a proxy may lead to your server ip being banned there are 3 ways to get around this : + +1. Use a proxy : [Find One Here](https://github.com/search?q=m3u8+proxy&type=repositories). +2. Get a server with rotating ip addresses or dynamic ip adresses and use the ips at random. +3. use a cloudflare proxy worker. \ No newline at end of file diff --git a/index.js b/index.js index f0da50c..c85c119 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,6 @@ import express from "express"; import { getEmbedSu } from "./src/extractors/embedsu.js"; +import { getTwoEmbed } from "./src/extractors/2Embed.js"; const port = 3000; @@ -7,12 +8,14 @@ const app = express() app.get('/', (req, res) => { res.status(200).json({ - intro: "Welcome to the unofficial vidsrcPro provider", - routes: { - movie: "/embedsu/:movieTMDBid", - show: "/embedsu/:showTMDBid?s=seasonNumber&e=episodeNumber" + INTRO: "Welcome to the unofficial vidsrcPro provider", + ROUTES: { + movie: "/embedsu|2embed/:movieTMDBid", + show: "/embedsu|2embed/:showTMDBid?s=seasonNumber&e=episodeNumber", + all_movie: "/combined/:movieTMDBid", + all_show: "/combined/:showTMDBid?s=seasonNumber&e=episodeNumber" }, - author: "This api is developed and created by Inside4ndroid Studios" + AUTHOR: "This api is developed and created by Inside4ndroid Studios" }); }); @@ -35,6 +38,54 @@ app.get('/embedsu/:tmdbId', async (req, res) => { } }); +app.get('/2embed/:tmdbId', async (req, res) => { + const id = req.params.tmdbId; + const season = req.query.s; + const episode = req.query.e; + + try { + if (season && episode) { + const vidsrcresponse = await getTwoEmbed(id, season, episode); + res.status(200).json(vidsrcresponse); + } else { + const vidsrcresponse = await getTwoEmbed(id, 0, 0); + res.status(200).json(vidsrcresponse); + } + } catch (error) { + console.error('Error fetching data:', error); + res.status(500).json({ error: 'Failed to fetch data' }); + } +}); + +app.get('/combined/:tmdbId', async (req, res) => { + const id = req.params.tmdbId; + const season = req.query.s; + const episode = req.query.e; + + try { + let embedSuResponse; + let twoEmbedResponse; + + if (season && episode) { + embedSuResponse = await getEmbedSu(id, season, episode); + twoEmbedResponse = await getTwoEmbed(id, season, episode); + } else { + embedSuResponse = await getEmbedSu(id); + twoEmbedResponse = await getTwoEmbed(id, 0, 0); + } + + const combinedResponse = { + embedsu: embedSuResponse, + twoembed: twoEmbedResponse, + }; + + res.status(200).json(combinedResponse); + } catch (error) { + console.error('Error fetching data:', error); + res.status(500).json({ error: 'Failed to fetch data' }); + } +}); + app.listen(port, () => { - console.log(`Example app listening on port http://localhost:${port}`); + console.log(`Api listening on port http://localhost:${port}`); }); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 84f6131..4a09a5a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "cheerio": "^1.0.0", "express": "^4.18.2", "fetch": "^1.1.0", "node-fetch": "^3.3.2" @@ -68,6 +69,12 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -96,6 +103,48 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/cheerio": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", + "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "encoding-sniffer": "^0.2.0", + "htmlparser2": "^9.1.0", + "parse5": "^7.1.2", + "parse5-htmlparser2-tree-adapter": "^7.0.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^6.19.5", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=18.17" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -132,6 +181,34 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -185,6 +262,61 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -208,6 +340,43 @@ "iconv-lite": "~0.4.13" } }, + "node_modules/encoding-sniffer": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", + "integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/encoding-sniffer/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", @@ -452,6 +621,25 @@ "node": ">= 0.4" } }, + "node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -602,6 +790,18 @@ "url": "https://opencollective.com/node-fetch" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/object-inspect": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", @@ -626,6 +826,43 @@ "node": ">= 0.8" } }, + "node_modules/parse5": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "license": "MIT", + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -855,6 +1092,15 @@ "node": ">= 0.6" } }, + "node_modules/undici": { + "version": "6.21.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.2.tgz", + "integrity": "sha512-uROZWze0R0itiAKVPsYhFov9LxrPMHLMEQFszeI2gCN6bnIIZ8twzBCJcN2LJrBBLfrP0t1FW0g+JmKVl8Vk1g==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -889,6 +1135,39 @@ "engines": { "node": ">= 8" } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } } } } diff --git a/package.json b/package.json index 988c69f..460cc64 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "author": "Inside4ndroid Studios Ltd", "license": "ISC", "dependencies": { + "cheerio": "^1.0.0", "express": "^4.18.2", "fetch": "^1.1.0", "node-fetch": "^3.3.2" diff --git a/src/extractors/2Embed.js b/src/extractors/2Embed.js new file mode 100644 index 0000000..eec6a1c --- /dev/null +++ b/src/extractors/2Embed.js @@ -0,0 +1,340 @@ +import fetch from "node-fetch"; +import JsUnpacker from '../utils/jsunpack.js'; +import * as cheerio from 'cheerio'; + +const DOMAIN = "https://www.2embed.cc"; +const PLAYER_URL = "https://uqloads.xyz"; + +let subtitles = []; +let sources = []; + +const headers = { + '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': DOMAIN, + 'Origin': DOMAIN, +}; + +function parseQuality(qualityString) { + if (!qualityString) return 0; + const q = qualityString.toUpperCase(); + if (q === '4K') return 4000; + if (q.includes('1080P')) return 1080; + if (q.includes('720P')) return 720; + const numMatch = q.match(/(\d+)/); + return numMatch ? parseInt(numMatch[1], 10) : 0; +} + +function createErrorObject(errorMessage) { + return { + provider: "2Embed", + ERROR: [{ + error: `ERROR`, + what_happened: errorMessage, + report_issue: 'https://github.com/Inside4ndroid/vidsrc-api-js/issues' + }] + }; +} + +export async function getTwoEmbed(tmdb_id, s, e) { + subtitles = []; + sources = []; + + const tmdbId = tmdb_id; + const initialReferer = s && e ? `${DOMAIN}/embedtv/${tmdbId}&s=${s}&e=${e}` : `${DOMAIN}/embed/${tmdbId}`; + const url = initialReferer; + + try { + const response = await fetch(url, { + method: "POST", + headers: { + Referer: url, + "Content-Type": "application/x-www-form-urlencoded", + "User-Agent": headers['User-Agent'], + }, + body: "pls=pls" + }); + + if (!response.ok) { + return { sources: [createErrorObject(`HTTP error fetching initial URL! Status: ${response.status}`)] }; + } + + const data = await response.text(); + let extractedValue = null; + let isSwishId = false; + + let match = data.match(/swish\?id=(?[\w\d]+)/); + + if (match && match.groups && match.groups.id) { + extractedValue = match.groups.id; + isSwishId = true; + } else { + match = data.match(/'(.*?player4u.*?)'/); + if (match && match[1]) { + extractedValue = match[1]; + } else { + return { sources: [createErrorObject("No relevant swish ID or player4u URL found in initial data.")] }; + } + } + + if (extractedValue) { + if (isSwishId) { + const streamUrl = await resolve(`${PLAYER_URL}/e/${extractedValue}`, DOMAIN); + if (!streamUrl) { + return { sources: [createErrorObject(`Could not resolve stream URL for swish ID: ${extractedValue}`)] }; + } + + const parsedSources = await parseM3U8(streamUrl, PLAYER_URL); + + if (parsedSources.length > 0) { + sources.push({ + provider: "2Embed/Swish", + files: parsedSources, + subtitles: subtitles, + headers: { + "Referer": PLAYER_URL, + "User-Agent": headers['User-Agent'], + "Origin": PLAYER_URL + } + }); + } else { + return { sources: [createErrorObject("No valid sources found in M3U8 for swish ID")] }; + } + } else { + const listPageResponse = await fetch(extractedValue, { + headers: { + Referer: url, + "User-Agent": headers['User-Agent'] + } + }); + + if (!listPageResponse.ok) { + return { sources: [createErrorObject(`Failed to fetch player4u list page (${extractedValue}): ${listPageResponse.status}`)] }; + } + + const listPageHtml = await listPageResponse.text(); + + const $ = cheerio.load(listPageHtml); + let highestQuality = -1; + let bestPartialUrl = null; + + $('li.slide-toggle a.playbtnx').each((index, element) => { + const linkText = $(element).text(); + const onclickAttr = $(element).attr('onclick'); + + if (!linkText || !onclickAttr) return; + + const qualityMatch = linkText.match(/\s*(\d+p|4K)\s*/i); + const qualityString = qualityMatch ? qualityMatch[1].toUpperCase() : null; + + const urlMatch = onclickAttr.match(/go\('([^']+)'\)/); + const partialUrl = urlMatch ? urlMatch[1] : null; + + if (!qualityString || !partialUrl) return; + + const qualityValue = parseQuality(qualityString); + + if (qualityValue > highestQuality) { + highestQuality = qualityValue; + bestPartialUrl = partialUrl; + } + }); + + if (bestPartialUrl) { + const idMatch = bestPartialUrl.match(/\?id=([\w\d]+)/); + if (idMatch && idMatch[1]) { + const player4uId = idMatch[1]; + const resolveUrl = `${PLAYER_URL}/e/${player4uId}`; + + const streamUrl = await resolve(resolveUrl, extractedValue); + if (!streamUrl) { + return { sources: [createErrorObject(`Could not resolve stream URL for player4u ID: ${player4uId}`)] }; + } + + const parsedSources = await parseM3U8(streamUrl, PLAYER_URL); + + if (parsedSources.length > 0) { + sources.push({ + provider: "2Embed/Player4u", + files: parsedSources, + subtitles: subtitles, + headers: { + "Referer": PLAYER_URL, + "User-Agent": headers['User-Agent'], + "Origin": PLAYER_URL + } + }); + } else { + return { sources: [createErrorObject("No valid sources found in M3U8 for player4u ID")] }; + } + } else { + return { sources: [createErrorObject("Could not extract player4u ID from best quality URL")] }; + } + } else { + return { sources: [createErrorObject("No valid quality options found on player4u page")] }; + } + } + } + + return { sources: sources.length > 0 ? { sources } : [createErrorObject("No sources found after processing")] }; + + } catch (error) { + return { sources: [createErrorObject(`Unexpected error: ${error.message}`)] }; + } +} + +async function resolve(url, referer) { + try { + const response = await fetch(url, { + headers: { + Referer: referer, + "User-Agent": headers['User-Agent'] + } + }); + + if (!response.ok) { + console.error(`Resolve failed for ${url}: Status ${response.status}`); + return null; + } + + const data = await response.text(); + const packedDataRegex = /eval\(function(.*?)split.*\)\)\)/s; + const packedDataMatch = data.match(packedDataRegex); + + if (packedDataMatch) { + const packedJS = packedDataMatch[0]; + const unpacker = new JsUnpacker(packedJS); + + if (unpacker.detect()) { + const unpackedJS = unpacker.unpack(); + if (!unpackedJS) { + console.error("JsUnpacker failed to unpack"); + return null; + } + + await parseSubs(unpackedJS); + + const fileRegex = /sources\s*:\s*\[\s*\{\s*file\s*:\s*"([^"]+)"/; + const matchUri = unpackedJS.match(fileRegex); + + if (matchUri && matchUri[1]) { + return matchUri[1]; + } else { + console.error("Could not find file URL in unpacked JS"); + return null; + } + } else { + console.error("JsUnpacker could not detect packed data in resolve response"); + return null; + } + } else { + console.error("No packed JS data found in resolve response for:", url); + if (url.includes('.m3u8')) return url; + return null; + } + } catch (error) { + console.error(`Error during resolve for ${url}:`, error); + return null; + } +} + +async function parseM3U8(m3u8Url, referer) { + const parsedSources = []; + try { + const response = await fetch(m3u8Url, { + headers: { + "User-Agent": headers['User-Agent'], + "Referer": referer + } + }); + + if (!response.ok) { + console.error(`Failed to fetch M3U8 ${m3u8Url}: Status ${response.status}`); + return []; + } + + const m3u8Content = await response.text(); + const lines = m3u8Content.split('\n'); + let currentQuality = null; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + + if (line.startsWith('#EXT-X-STREAM-INF:')) { + currentQuality = null; + const resolutionMatch = line.match(/RESOLUTION=\d+x(\d+)/); + if (resolutionMatch && resolutionMatch[1]) { + currentQuality = resolutionMatch[1] + "p"; + } else { + const bwMatch = line.match(/BANDWIDTH=(\d+)/); + if (bwMatch) { + currentQuality = `bw${bwMatch[1]}`; + } + } + } else if (currentQuality && (line.startsWith('http') || (line.includes('.m3u8') && !line.startsWith('#')))) { + let resolvedUrl = line; + if (!line.startsWith('http')) { + const baseUrl = new URL(m3u8Url); + resolvedUrl = new URL(line, baseUrl.origin + baseUrl.pathname.substring(0, baseUrl.pathname.lastIndexOf('/') + 1)).href; + } + + parsedSources.push({ + file: resolvedUrl, + type: 'hls', + quality: currentQuality, + lang: 'en' + }); + currentQuality = null; + } + } + parsedSources.sort((a, b) => parseQuality(b.quality) - parseQuality(a.quality)); + + return parsedSources; + } catch (error) { + console.error(`Error parsing M3U8 ${m3u8Url}:`, error); + return []; + } +} + +async function parseSubs(scriptstring) { + if (typeof subtitles === 'undefined') { + console.error("Subtitles array is not accessible in parseSubs"); + return; + } + + try { + const setupMatch = scriptstring.match(/jwplayer\([\s\S]*?\)\.setup\((\{[\s\S]*?\})\)/); + if (!setupMatch || !setupMatch[1]) { + return; + } + + let setupStr = setupMatch[1]; + + setupStr = setupStr.replace(/\\'/g, "'"); + setupStr = setupStr.replace(/'/g, '"'); + setupStr = setupStr.replace(/([{,]\s*)([a-zA-Z0-9_$]+)(\s*:)/g, '$1"$2"$3'); + setupStr = setupStr.replace(/,\s*([}\]])/g, '$1'); + + let setupConfig; + try { + setupConfig = JSON.parse(setupStr); + } catch (err) { + return; + } + + const subtitleTracks = (setupConfig.tracks || []).filter(track => + track && (track.kind === "captions" || track.kind === "subtitles") && track.file + ).map(track => ({ + url: track.file, + lang: track.label || track.kind + })); + + subtitleTracks.forEach(newSub => { + if (!subtitles.some(existingSub => existingSub.url === newSub.url && existingSub.lang === newSub.lang)) { + subtitles.push(newSub); + } + }); + + } catch (error) { + console.error("Error during subtitle parsing:", error); + } +} \ No newline at end of file diff --git a/src/extractors/embedsu.js b/src/extractors/embedsu.js index 19ea880..fb5e40a 100644 --- a/src/extractors/embedsu.js +++ b/src/extractors/embedsu.js @@ -1,5 +1,16 @@ import fetch from 'node-fetch'; +function createErrorObject(errorMessage) { + return { + provider: "EmbedSU", + ERROR: [{ + error: `ERROR`, + what_happened: errorMessage, + report_issue: 'https://github.com/Inside4ndroid/vidsrc-api-js/issues' + }] + }; +} + export async function getEmbedSu(tmdb_id, s, e) { const DOMAIN = "https://embed.su"; const headers = { @@ -11,82 +22,133 @@ export async function getEmbedSu(tmdb_id, s, e) { try { const urlSearch = s && e ? `${DOMAIN}/embed/tv/${tmdb_id}/${s}/${e}` : `${DOMAIN}/embed/movie/${tmdb_id}`; const htmlSearch = await fetch(urlSearch, { method: 'GET', headers }); - const textSearch = await htmlSearch.text(); + if (!htmlSearch.ok) { + return { sources: [createErrorObject(`Failed to fetch initial page: HTTP ${htmlSearch.status}`)] }; + } + + const textSearch = await htmlSearch.text(); const hashEncodeMatch = textSearch.match(/JSON\.parse\(atob\(\`([^\`]+)/i); const hashEncode = hashEncodeMatch ? hashEncodeMatch[1] : ""; - if (!hashEncode) return; + if (!hashEncode) { + return { sources: [createErrorObject("No encoded hash found in initial page")] }; + } + + let hashDecode; + try { + hashDecode = JSON.parse(await stringAtob(hashEncode)); + } catch (e) { + return { sources: [createErrorObject(`Failed to decode initial hash: ${e.message}`)] }; + } - const hashDecode = JSON.parse(await stringAtob(hashEncode)); const mEncrypt = hashDecode.hash; - if (!mEncrypt) return; + if (!mEncrypt) { + return { sources: [createErrorObject("No encrypted hash found in decoded data")] }; + } - const firstDecode = (await stringAtob(mEncrypt)).split(".").map(item => item.split("").reverse().join("")); - const secondDecode = JSON.parse(await stringAtob(firstDecode.join("").split("").reverse().join(""))); + let firstDecode; + try { + firstDecode = (await stringAtob(mEncrypt)).split(".").map(item => item.split("").reverse().join("")); + } catch (e) { + return { sources: [createErrorObject(`Failed to decode first layer: ${e.message}`)] }; + } - if (!secondDecode || secondDecode.length === 0) return; + let secondDecode; + try { + secondDecode = JSON.parse(await stringAtob(firstDecode.join("").split("").reverse().join(""))); + } catch (e) { + return { sources: [createErrorObject(`Failed to decode second layer: ${e.message}`)] }; + } + + if (!secondDecode || secondDecode.length === 0) { + return { sources: [createErrorObject("No valid sources found after decoding")] }; + } const sources = []; const subtitles = []; for (const item of secondDecode) { - const urlDirect = `${DOMAIN}/api/e/${item.hash}`; - const dataDirect = await requestGet(urlDirect, { - "Referer": DOMAIN, - "User-Agent": headers['User-Agent'], - "Origin": DOMAIN - }); - - if (!dataDirect.source) continue; - - const tracks = dataDirect.subtitles.map(sub => ({ - url: sub.file, - lang: sub.label.split('-')[0].trim() - })).filter(track => track.lang); - - const requestDirectSize = await fetch(dataDirect.source, { headers, method: "GET" }); - const parseRequest = await requestDirectSize.text(); - - const patternSize = parseRequest.split('\n').filter(item => item.includes('/proxy/')); - - const directQuality = patternSize.map(patternItem => { - const sizeQuality = getSizeQuality(patternItem); - let dURL = `${DOMAIN}${patternItem}`; - dURL = dURL.replace(".png", ".m3u8"); - return { file: dURL, type: 'hls', quality: `${sizeQuality}p`, lang: 'en' }; - }); - - if (!directQuality.length) continue; - - sources.push({ - provider: "EmbedSu", - files: directQuality, - headers: { + try { + const urlDirect = `${DOMAIN}/api/e/${item.hash}`; + const dataDirect = await requestGet(urlDirect, { "Referer": DOMAIN, "User-Agent": headers['User-Agent'], "Origin": DOMAIN - } - }); + }); - subtitles.push(...tracks); + if (!dataDirect || !dataDirect.source) { + console.warn(`No source found for hash ${item.hash}`); + continue; + } + + const tracks = (dataDirect.subtitles || []).map(sub => ({ + url: sub.file, + lang: sub.label ? sub.label.split('-')[0].trim() : 'en' + })).filter(track => track.url); + + const requestDirectSize = await fetch(dataDirect.source, { headers, method: "GET" }); + if (!requestDirectSize.ok) { + console.warn(`Failed to fetch source ${dataDirect.source}: HTTP ${requestDirectSize.status}`); + continue; + } + + const parseRequest = await requestDirectSize.text(); + const patternSize = parseRequest.split('\n').filter(item => item.includes('/proxy/')); + + const directQuality = patternSize.map(patternItem => { + try { + const sizeQuality = getSizeQuality(patternItem); + let dURL = `${DOMAIN}${patternItem}`; + dURL = dURL.replace(".png", ".m3u8"); + return { file: dURL, type: 'hls', quality: `${sizeQuality}p`, lang: 'en' }; + } catch (e) { + console.warn(`Failed to process quality for pattern: ${patternItem}`, e); + return null; + } + }).filter(item => item !== null); + + if (!directQuality.length) { + console.warn(`No valid qualities found for source ${dataDirect.source}`); + continue; + } + + sources.push({ + provider: "EmbedSu", + files: directQuality, + subtitles: tracks, + headers: { + "Referer": DOMAIN, + "User-Agent": headers['User-Agent'], + "Origin": DOMAIN + } + }); + } catch (e) { + console.error(`Error processing item ${item.hash}:`, e); + // Continue to next item instead of failing entire request + } } - return { - sources, - subtitles - }; + if (sources.length === 0) { + return { sources: [createErrorObject("No valid sources found after processing all items")] }; + } + + return { sources }; } catch (e) { - return { provider: "EmbedSu", sources: [], subtitles: [] }; + return { sources: [createErrorObject(`Unexpected error: ${e.message}`)] }; } } function getSizeQuality(url) { - const parts = url.split('/'); - const base64Part = parts[parts.length - 2]; - const decodedPart = atob(base64Part); - const sizeQuality = Number(decodedPart) || 1080; - return sizeQuality; + try { + const parts = url.split('/'); + const base64Part = parts[parts.length - 2]; + const decodedPart = atob(base64Part); + return Number(decodedPart) || 1080; + } catch (e) { + console.warn(`Failed to get size quality for URL ${url}:`, e); + return 720; // Default quality if parsing fails + } } async function stringAtob(input) { @@ -115,6 +177,7 @@ async function requestGet(url, headers = {}) { return await response.json(); } catch (error) { - return ""; + console.error(`Request failed for ${url}:`, error); + return null; } } \ No newline at end of file diff --git a/src/utils/jsunpack.js b/src/utils/jsunpack.js new file mode 100644 index 0000000..ca7360d --- /dev/null +++ b/src/utils/jsunpack.js @@ -0,0 +1,106 @@ +export default class JsUnpacker { + static Unbase = class { + constructor(radix) { + this.ALPHABET_62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + this.ALPHABET_95 = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"; + this.alphabet = null; + this.dictionary = {}; + this.radix = radix; + + if (radix > 36) { + if (radix < 62) { + this.alphabet = this.ALPHABET_62.substring(0, radix); + } else if (radix > 62 && radix < 95) { + this.alphabet = this.ALPHABET_95.substring(0, radix); + } else if (radix === 62) { + this.alphabet = this.ALPHABET_62; + } else if (radix === 95) { + this.alphabet = this.ALPHABET_95; + } + + for (let i = 0; i < this.alphabet.length; i++) { + this.dictionary[this.alphabet[i]] = i; + } + } + } + + unbase(str) { + let ret = 0; + + if (this.alphabet === null) { + ret = parseInt(str, this.radix); + } else { + const tmp = str.split('').reverse().join(''); + for (let i = 0; i < tmp.length; i++) { + ret += Math.pow(this.radix, i) * this.dictionary[tmp[i]]; + } + } + return ret; + } + }; + + constructor(packedJS) { + this.packedJS = packedJS; + } + + detect() { + const js = this.packedJS.replace(/\s/g, ''); + const regex = /eval\(function\(p,a,c,k,e,(?:r|d)/; + return regex.test(js); + } + + unpack() { + let js = this.packedJS; + try { + const regex = /\}\s*\('(.*)',\s*(.*?),\s*(\d+),\s*'(.*?)'\.split\('\|'\)/s; + const match = js.match(regex); + if (match && match.length === 5) { + let payload = match[1].replace(/\\'/g, "'"); + const radixStr = match[2]; + const countStr = match[3]; + const symtab = match[4].split('|'); + + let radix = 36; + let count = 0; + try { + radix = parseInt(radixStr); + } catch (e) { + } + try { + count = parseInt(countStr); + } catch (e) { + } + + if (symtab.length !== count) { + throw new Error("Unknown p.a.c.k.e.r. encoding"); + } + + const unbase = new this.constructor.Unbase(radix); + const wordRegex = /\b\w+\b/g; + let decoded = payload; + let replaceOffset = 0; + let wordMatch; + + while ((wordMatch = wordRegex.exec(payload)) !== null) { + const word = wordMatch[0]; + const x = unbase.unbase(word); + let value = null; + if (x < symtab.length) { + value = symtab[x]; + } + + if (value && value.length > 0) { + const start = wordMatch.index + replaceOffset; + const end = start + word.length; + decoded = decoded.substring(0, start) + value + decoded.substring(end); + replaceOffset += (value.length - word.length); + } + } + return decoded; + } + } catch (e) { + return null; + } + return null; + } +} \ No newline at end of file