diff --git a/README.md b/README.md index c991f5c..0f0d08b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Unoffical Vidsrc (embed.su) API! 👋 +# Unoffical vidsrc (embed.su) API! 👋 ## Sponsorship / Donations Your sponsorship is vital in helping me achieve this mission. With your support, I can: @@ -44,18 +44,17 @@ TVSHOWS : The providers are currently : ``` -vidsrc -asiaheroku +embedsu ``` ## Example Movie ``` -https://your-domain/vidsrc/916224 +https://your-domain/embedsu/916224 ``` ## Example Show ``` -https://your-domain/asiaheroku/1429?s=1&e=1 +https://your-domain/embedsu/1429?s=1&e=1 ``` ## Deployment diff --git a/index.js b/index.js index 239d7ab..de0ec9d 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,5 @@ import express from "express"; -import { getvidsrc } from "./src/vidsrcpro.js"; +import { getEmbedSu } from "./src/embedsu.js"; const port = 3000; @@ -9,24 +9,24 @@ app.get('/', (req, res) => { res.status(200).json({ intro: "Welcome to the unofficial vidsrcPro provider", routes: { - movie: "/vidsrc/:movieTMDBid", - show: "/vidsrc/:showTMDBid?s=seasonNumber&e=episodeNumber" + movie: "/embedsu/:movieTMDBid", + show: "/embedsu/:showTMDBid?s=seasonNumber&e=episodeNumber" }, author: "This api is developed and created by Inside4ndroid Studios" }); }); -app.get('/vidsrc/:tmdbId', async (req, res) => { +app.get('/embedsu/: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 getvidsrc(id, season, episode); + const vidsrcresponse = await getEmbedSu(id, season, episode); res.status(200).json(vidsrcresponse); } else { - const vidsrcresponse = await getvidsrc(id); + const vidsrcresponse = await getEmbedSu(id); res.status(200).json(vidsrcresponse); } } catch (error) { diff --git a/package-lock.json b/package-lock.json index b57d177..84f6131 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,9 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "express": "^4.18.2" + "express": "^4.18.2", + "fetch": "^1.1.0", + "node-fetch": "^3.3.2" } }, "node_modules/accepts": { @@ -31,6 +33,17 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/biskviit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/biskviit/-/biskviit-1.0.1.tgz", + "integrity": "sha512-VGCXdHbdbpEkFgtjkeoBN8vRlbj1ZRX2/mxhE8asCCRalUx2nBzOomLJv8Aw/nRt5+ccDb+tPKidg4XxcfGW4w==", + "dependencies": { + "psl": "^1.1.7" + }, + "engines": { + "node": ">=1.0.0" + } + }, "node_modules/body-parser": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", @@ -119,6 +132,14 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, + "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", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "engines": { + "node": ">= 12" + } + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -179,6 +200,14 @@ "node": ">= 0.8" } }, + "node_modules/encoding": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", + "integrity": "sha512-bl1LAgiQc4ZWr++pNYUdRe/alecaHFeHxIJ/pNciqGdKXghaTCOwKkbKp6ye7pKZGu/GcaSXFk8PBVhgs+dJdA==", + "dependencies": { + "iconv-lite": "~0.4.13" + } + }, "node_modules/es-define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", @@ -257,6 +286,37 @@ "node": ">= 0.10.0" } }, + "node_modules/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-5O8TwrGzoNblBG/jtK4NFuZwNCkZX6s5GfRNOaGtm+QGJEuNakSC/i2RW0R93KX6E0jVjNXm6O3CRN4Ql3K+yA==", + "dependencies": { + "biskviit": "1.0.1", + "encoding": "0.1.12" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/finalhandler": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", @@ -275,6 +335,17 @@ "node": ">= 0.8" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -496,6 +567,41 @@ "node": ">= 0.6" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/object-inspect": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", @@ -548,6 +654,25 @@ "node": ">= 0.10" } }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -756,6 +881,14 @@ "engines": { "node": ">= 0.8" } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "engines": { + "node": ">= 8" + } } } } diff --git a/package.json b/package.json index 172de9d..988c69f 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,8 @@ "author": "Inside4ndroid Studios Ltd", "license": "ISC", "dependencies": { - "express": "^4.18.2" + "express": "^4.18.2", + "fetch": "^1.1.0", + "node-fetch": "^3.3.2" } } diff --git a/src/embedsu.js b/src/embedsu.js new file mode 100644 index 0000000..f17287a --- /dev/null +++ b/src/embedsu.js @@ -0,0 +1,123 @@ + import fetch from 'node-fetch'; +import { languageMap } from './languages.js'; + +export async function getEmbedSu(tmdb_id, s, e) { + const DOMAIN = "https://embed.su"; + 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, + }; + + 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(); + + const hashEncodeMatch = textSearch.match(/JSON\.parse\(atob\(\`([^\`]+)/i); + const hashEncode = hashEncodeMatch ? hashEncodeMatch[1] : ""; + + if (!hashEncode) return; + + const hashDecode = JSON.parse(await stringAtob(hashEncode)); + const mEncrypt = hashDecode.hash; + if (!mEncrypt) return; + + const firstDecode = (await stringAtob(mEncrypt)).split(".").map(item => item.split("").reverse().join("")); + const secondDecode = JSON.parse(await stringAtob(firstDecode.join("").split("").reverse().join(""))); + + if (!secondDecode || secondDecode.length === 0) return; + + const sources = []; + const subtitles = []; + + for (const item of secondDecode) { + if (item.name.toLowerCase() !== "viper") continue; + + const urlDirect = `${DOMAIN}/api/e/${item.hash}`; + const dataDirect = await requestGet(urlDirect, { + "Referer": DOMAIN, + "User-Agent": headers['User-Agent'], + "Accept": "*/*" + }); + + if (!dataDirect.source) continue; + + const tracks = dataDirect.subtitles.map(sub => ({ + url: sub.file, + lang: languageMap[sub.label.split(' ')[0]] || sub.label + })).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("embed.su/api/proxy/viper/", "").replace(".png", ".m3u8"); + return { file: dURL, type: 'hls', quality: `${sizeQuality}p`, lang: 'en' }; + }); + + if (!directQuality.length) continue; + + sources.push({ + provider: "EmbedSu", + files: directQuality, + headers: { + "Referer": DOMAIN, + "User-Agent": headers['User-Agent'], + "Origin": DOMAIN + } + }); + + subtitles.push(...tracks); + } + + return { + sources, + subtitles + }; + } catch (e) { + return { provider: "EmbedSu", sources: [], subtitles: [] }; + } +} + +function getSizeQuality(url) { + const parts = url.split('/'); + const base64Part = parts[parts.length - 2]; + const decodedPart = atob(base64Part); + const sizeQuality = Number(decodedPart) || 1080; + return sizeQuality; +} + +async function stringAtob(input) { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + let str = input.replace(/=+$/, ''); + let output = ''; + + if (str.length % 4 === 1) { + throw new Error("'atob' failed: The string to be decoded is not correctly encoded."); + } + + for (let bc = 0, bs = 0, buffer, i = 0; buffer = str.charAt(i++); ~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer, bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0) { + buffer = chars.indexOf(buffer); + } + + return output; +} + +async function requestGet(url, headers = {}) { + try { + const response = await fetch(url, { method: 'GET', headers }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return await response.json(); + } catch (error) { + return ""; + } +} \ No newline at end of file diff --git a/src/languages.js b/src/languages.js new file mode 100644 index 0000000..b9078c6 --- /dev/null +++ b/src/languages.js @@ -0,0 +1,126 @@ +export const languageMap = { + "Abkhazian": "ab", + "Afar": "aa", + "Afrikaans": "af", + "Albanian": "sq", + "Amharic": "am", + "Arabic": "ar", + "Aragonese": "an", + "Armenian": "hy", + "Assamese": "as", + "Aymara": "ay", + "Azerbaijani": "az", + "Bashkir": "ba", + "Basque": "eu", + "Bengali": "bn", + "Bihari": "bh", + "Bislama": "bi", + "Bosnian": "bs", + "Breton": "br", + "Bulgarian": "bg", + "Burmese": "my", + "Catalan": "ca", + "Chinese": "zh", + "Chuvash": "cv", + "Cornish": "kw", + "Corsican": "co", + "Croatian": "hr", + "Czech": "cs", + "Danish": "da", + "Dutch": "nl", + "Dzongkha": "dz", + "English": "en", + "Esperanto": "eo", + "Estonian": "et", + "Ewe": "ee", + "Fijian": "fj", + "Finnish": "fi", + "French": "fr", + "Georgian": "ka", + "German": "de", + "Greek": "el", + "Guaraní": "gn", + "Gujarati": "gu", + "Haitian Creole": "ht", + "Hausa": "ha", + "Hawaiian": "haw", + "Hebrew": "he", + "Hindi": "hi", + "Hmong": "hmn", + "Hungarian": "hu", + "Icelandic": "is", + "Igbo": "ig", + "Indonesian": "id", + "Interlingua": "ia", + "Irish": "ga", + "Italian": "it", + "Japanese": "ja", + "Javanese": "jv", + "Kannada": "kn", + "Kazakh": "kk", + "Khmer": "km", + "Kinyarwanda": "rw", + "Korean": "ko", + "Kurdish": "ku", + "Kyrgyz": "ky", + "Lao": "lo", + "Latin": "la", + "Latvian": "lv", + "Lingala": "ln", + "Lithuanian": "lt", + "Luxembourgish": "lb", + "Macedonian": "mk", + "Malagasy": "mg", + "Malay": "ms", + "Malayalam": "ml", + "Maltese": "mt", + "Maori": "mi", + "Marathi": "mr", + "Mongolian": "mn", + "Nepali": "ne", + "Norwegian": "no", + "Occitan": "oc", + "Oromo": "om", + "Pashto": "ps", + "Persian": "fa", + "Polish": "pl", + "Portuguese": "pt", + "Quechua": "qu", + "Romanian": "ro", + "Russian": "ru", + "Samoan": "sm", + "Sanskrit": "sa", + "Scots Gaelic": "gd", + "Serbian": "sr", + "Sesotho": "st", + "Shona": "sn", + "Sindhi": "sd", + "Sinhalese": "si", + "Slovak": "sk", + "Slovenian": "sl", + "Somali": "so", + "Spanish": "es", + "Sundanese": "su", + "Swahili": "sw", + "Swedish": "sv", + "Tajik": "tg", + "Tamil": "ta", + "Tatar": "tt", + "Telugu": "te", + "Thai": "th", + "Tigrinya": "ti", + "Turkish": "tr", + "Turkmen": "tk", + "Ukrainian": "uk", + "Urdu": "ur", + "Uzbek": "uz", + "Vietnamese": "vi", + "Volapük": "vo", + "Walloon": "wa", + "Welsh": "cy", + "Wolof": "wo", + "Xhosa": "xh", + "Yiddish": "yi", + "Yoruba": "yo", + "Zulu": "zu" +}; \ No newline at end of file diff --git a/src/vidsrcpro.js b/src/vidsrcpro.js deleted file mode 100644 index 308cf5b..0000000 --- a/src/vidsrcpro.js +++ /dev/null @@ -1,174 +0,0 @@ -export async function getvidsrc(tmdb_id, s, e) { - const DOMAIN = "https://embed.su"; - 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}`, - }; - - try { - let urlSearch = ''; - - if(s && e){ - urlSearch = `${DOMAIN}/embed/tv/${tmdb_id}/${s}/${e}`; - } else { - urlSearch = `${DOMAIN}/embed/movie/${tmdb_id}`; - } - - const htmlSearch = await fetch(urlSearch, { - method: 'GET', - headers: headers, - }); - const textSearch = await htmlSearch.text(); - - const hashEncodeMatch = textSearch.match(/JSON\.parse\(atob\(\`([^\`]+)/i); - const hashEncode = hashEncodeMatch ? hashEncodeMatch[1] : ""; - - if (!hashEncode) { - return; - } - - const hashDecode = JSON.parse(await string_atob(hashEncode)); - - const mEncrypt = hashDecode.hash; - if (!mEncrypt) { - return; - } - - const firstDecode = (await string_atob(mEncrypt)).split(".").map(item => item.split("").reverse().join("")); - const secondDecode = JSON.parse(await string_atob(firstDecode.join("").split("").reverse().join(""))); - - if (!secondDecode || secondDecode.length == 0) { - return; - } - - for (let i = 0; i < secondDecode.length; i++) { - const item = secondDecode[i]; - if (item.name.toLowerCase() != "viper") { - continue; - } - - const urlDirect = `${DOMAIN}/api/e/${item.hash}`; - const dataDirect = await request_get(urlDirect, { - "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", - "Accept": "*/*" - }, false); - - if (!dataDirect.source) { - continue; - } - - const tracks = []; - try { - for (let j = 0; j < dataDirect.subtitles.length; j++) { - const itemTrack = dataDirect.subtitles[j]; - const labelMatch = itemTrack.label.match(/^([A-z]+)/i); - const label = labelMatch ? labelMatch[1] : ""; - if (!label) { - continue; - } - tracks.push({ - url: itemTrack.file, - lang: label - }); - } - } catch (etrack) { } - - const requestDirectSize = await fetch(dataDirect.source, { - headers: { - "Referer": `${DOMAIN}`, - "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", - "Accept": "*/*" - }, - method: "GET", - }); - const parseRequest = await requestDirectSize.text(); - - const patternSize = []; - const parseDirectSize = parseRequest.split('\n'); - - for (let k = 0; k < parseDirectSize.length; k++) { - const item = parseDirectSize[k]; - if (item.indexOf('/proxy/') == -1) { - continue; - } - patternSize.push(item); - } - - const directQuality = []; - for (let l = 0; l < patternSize.length; l++) { - const patternItem = patternSize[l]; - const sizeQualityMatch = patternItem.match(/\/([0-9]+)\//i); - const sizeQuality = sizeQualityMatch ? Number(sizeQualityMatch[1]) : 1080; - let dURL = `${DOMAIN}${patternItem}`; - dURL = dURL.replace("embed.su/api/proxy/viper/", ""); - dURL = dURL.replace(".png", ".m3u8"); - directQuality.push({ - url: dURL, - quality: sizeQuality.toString(), - isM3U8: true - }); - } - - if (!directQuality.length) { - continue; - } - - directQuality.sort((a, b) => b.quality - a.quality); - - const results = { - 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", - "Accept": "*/*" - }, - sources: directQuality, - subtitles: tracks - }; - - return results; - } - } catch (e) { - const results = { - headers: {}, - sources: '', - subtitles: '' - }; - return results; - } -} - -async function string_atob(input) { - var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; - var str = input.replace(/=+$/, ''); - var output = ''; - if (str.length % 4 == 1) { - throw new Error("'atob' failed: The string to be decoded is not correctly encoded."); - } - for (var bc = 0, bs = 0, buffer = void 0, i = 0; buffer = str.charAt(i++); ~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer, - bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0) { - buffer = chars.indexOf(buffer); - } - return output; -} - -async function request_get(url, headers = {}) { - try { - headers = headers || {}; - const response = await fetch(url, { - method: 'GET', - headers: headers, - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); - - return data; - } catch (error) { - return ""; - } -} \ No newline at end of file