1 changed files with 251 additions and 0 deletions
@ -0,0 +1,251 @@ |
|||||||
|
import Base64 from "crypto-js/enc-base64"; |
||||||
|
import Utf8 from "crypto-js/enc-utf8"; |
||||||
|
|
||||||
|
import { proxiedFetch, rawProxiedFetch } from "../helpers/fetch"; |
||||||
|
import { registerProvider } from "../helpers/register"; |
||||||
|
import { |
||||||
|
MWCaptionType, |
||||||
|
MWStreamQuality, |
||||||
|
MWStreamType, |
||||||
|
} from "../helpers/streams"; |
||||||
|
import { MWMediaType } from "../metadata/types"; |
||||||
|
|
||||||
|
const twoEmbedBase = "https://www.2embed.to"; |
||||||
|
|
||||||
|
async function fetchCaptchaToken(recaptchaKey: string) { |
||||||
|
const domainHash = Base64.stringify(Utf8.parse(twoEmbedBase)).replace( |
||||||
|
/=/g, |
||||||
|
"." |
||||||
|
); |
||||||
|
|
||||||
|
const recaptchaRender = await proxiedFetch<any>( |
||||||
|
`https://www.google.com/recaptcha/api.js?render=${recaptchaKey}` |
||||||
|
); |
||||||
|
|
||||||
|
const vToken = recaptchaRender.substring( |
||||||
|
recaptchaRender.indexOf("/releases/") + 10, |
||||||
|
recaptchaRender.indexOf("/recaptcha__en.js") |
||||||
|
); |
||||||
|
|
||||||
|
const recaptchaAnchor = await proxiedFetch<any>( |
||||||
|
`https://www.google.com/recaptcha/api2/anchor?ar=1&hl=en&size=invisible&cb=flicklax&k=${recaptchaKey}&co=${domainHash}&v=${vToken}` |
||||||
|
); |
||||||
|
|
||||||
|
const cToken = new DOMParser() |
||||||
|
.parseFromString(recaptchaAnchor, "text/html") |
||||||
|
.getElementById("recaptcha-token") |
||||||
|
?.getAttribute("value"); |
||||||
|
|
||||||
|
if (!cToken) throw new Error("Unable to find cToken"); |
||||||
|
|
||||||
|
const payload = { |
||||||
|
v: vToken, |
||||||
|
reason: "q", |
||||||
|
k: recaptchaKey, |
||||||
|
c: cToken, |
||||||
|
sa: "", |
||||||
|
co: twoEmbedBase, |
||||||
|
}; |
||||||
|
|
||||||
|
const tokenData = await proxiedFetch<string>( |
||||||
|
`https://www.google.com/recaptcha/api2/reload?${new URLSearchParams( |
||||||
|
payload |
||||||
|
).toString()}`,
|
||||||
|
{ |
||||||
|
headers: { referer: "https://www.google.com/recaptcha/api2/" }, |
||||||
|
method: "POST", |
||||||
|
} |
||||||
|
); |
||||||
|
|
||||||
|
const token = tokenData.match('rresp","(.+?)"'); |
||||||
|
return token ? token[1] : null; |
||||||
|
} |
||||||
|
|
||||||
|
interface IEmbedRes { |
||||||
|
link: string; |
||||||
|
sources: []; |
||||||
|
tracks: []; |
||||||
|
type: string; |
||||||
|
} |
||||||
|
|
||||||
|
interface IStreamData { |
||||||
|
status: string; |
||||||
|
message: string; |
||||||
|
type: string; |
||||||
|
token: string; |
||||||
|
result: |
||||||
|
| { |
||||||
|
Original: { |
||||||
|
label: string; |
||||||
|
file: string; |
||||||
|
url: string; |
||||||
|
}; |
||||||
|
} |
||||||
|
| { |
||||||
|
label: string; |
||||||
|
size: number; |
||||||
|
url: string; |
||||||
|
}[]; |
||||||
|
} |
||||||
|
|
||||||
|
interface ISubtitles { |
||||||
|
url: string; |
||||||
|
lang: string; |
||||||
|
} |
||||||
|
|
||||||
|
async function fetchStream(sourceId: string, captchaToken: string) { |
||||||
|
const embedRes = await proxiedFetch<IEmbedRes>( |
||||||
|
`${twoEmbedBase}/ajax/embed/play?id=${sourceId}&_token=${captchaToken}`, |
||||||
|
{ |
||||||
|
headers: { |
||||||
|
Referer: twoEmbedBase, |
||||||
|
}, |
||||||
|
} |
||||||
|
); |
||||||
|
|
||||||
|
// Link format: https://rabbitstream.net/embed-4/{data-id}?z=
|
||||||
|
const rabbitStreamUrl = new URL(embedRes.link); |
||||||
|
|
||||||
|
const dataPath = rabbitStreamUrl.pathname.split("/"); |
||||||
|
const dataId = dataPath[dataPath.length - 1]; |
||||||
|
|
||||||
|
// https://rabbitstream.net/embed/m-download/{data-id}
|
||||||
|
const download = await proxiedFetch<any>( |
||||||
|
`${rabbitStreamUrl.origin}/embed/m-download/${dataId}`, |
||||||
|
{ |
||||||
|
headers: { |
||||||
|
referer: twoEmbedBase, |
||||||
|
}, |
||||||
|
} |
||||||
|
); |
||||||
|
|
||||||
|
const downloadPage = new DOMParser().parseFromString(download, "text/html"); |
||||||
|
|
||||||
|
const streamlareEl = Array.from( |
||||||
|
downloadPage.querySelectorAll(".dls-brand") |
||||||
|
).find((el) => el.textContent?.trim() === "Streamlare"); |
||||||
|
if (!streamlareEl) throw new Error("Unable to find streamlare element"); |
||||||
|
|
||||||
|
const streamlareUrl = |
||||||
|
streamlareEl.nextElementSibling?.querySelector("a")?.href; |
||||||
|
if (!streamlareUrl) throw new Error("Unable to parse streamlare url"); |
||||||
|
|
||||||
|
const subtitles: ISubtitles[] = []; |
||||||
|
const subtitlesDropdown = downloadPage.querySelectorAll( |
||||||
|
"#user_menu .dropdown-item" |
||||||
|
); |
||||||
|
subtitlesDropdown.forEach((item) => { |
||||||
|
const url = item.getAttribute("href"); |
||||||
|
const lang = item.textContent?.trim().replace("Download", "").trim(); |
||||||
|
if (url && lang) subtitles.push({ url, lang }); |
||||||
|
}); |
||||||
|
|
||||||
|
const streamlare = await proxiedFetch<any>(streamlareUrl); |
||||||
|
|
||||||
|
const streamlarePage = new DOMParser().parseFromString( |
||||||
|
streamlare, |
||||||
|
"text/html" |
||||||
|
); |
||||||
|
|
||||||
|
const csrfToken = streamlarePage |
||||||
|
.querySelector("head > meta:nth-child(3)") |
||||||
|
?.getAttribute("content"); |
||||||
|
|
||||||
|
if (!csrfToken) throw new Error("Unable to find CSRF token"); |
||||||
|
|
||||||
|
const videoId = streamlareUrl.match("/[ve]/([^?#&/]+)")?.[1]; |
||||||
|
if (!videoId) throw new Error("Unable to get streamlare video id"); |
||||||
|
|
||||||
|
const streamRes = await proxiedFetch<IStreamData>( |
||||||
|
`${new URL(streamlareUrl).origin}/api/video/download/get`, |
||||||
|
{ |
||||||
|
method: "POST", |
||||||
|
body: JSON.stringify({ |
||||||
|
id: videoId, |
||||||
|
}), |
||||||
|
headers: { |
||||||
|
"X-Requested-With": "XMLHttpRequest", |
||||||
|
"X-CSRF-Token": csrfToken, |
||||||
|
}, |
||||||
|
} |
||||||
|
); |
||||||
|
|
||||||
|
if (streamRes.message !== "OK") throw new Error("Unable to fetch stream"); |
||||||
|
|
||||||
|
const streamData = Array.isArray(streamRes.result) |
||||||
|
? streamRes.result[0] |
||||||
|
: streamRes.result.Original; |
||||||
|
if (!streamData) throw new Error("Unable to get stream data"); |
||||||
|
|
||||||
|
const followStream = await rawProxiedFetch(streamData.url, { |
||||||
|
method: "HEAD", |
||||||
|
referrer: new URL(streamlareUrl).origin, |
||||||
|
}); |
||||||
|
|
||||||
|
const finalStreamUrl = followStream.headers.get("X-Final-Destination"); |
||||||
|
return { url: finalStreamUrl, subtitles }; |
||||||
|
} |
||||||
|
|
||||||
|
registerProvider({ |
||||||
|
id: "2embed", |
||||||
|
displayName: "2Embed", |
||||||
|
rank: 125, |
||||||
|
type: [MWMediaType.MOVIE, MWMediaType.SERIES], |
||||||
|
async scrape({ media, episode, progress }) { |
||||||
|
let embedUrl = `${twoEmbedBase}/embed/tmdb/movie?id=${media.tmdbId}`; |
||||||
|
|
||||||
|
if (media.meta.type === MWMediaType.SERIES) { |
||||||
|
const seasonNumber = media.meta.seasonData.number; |
||||||
|
const episodeNumber = media.meta.seasonData.episodes.find( |
||||||
|
(e) => e.id === episode |
||||||
|
)?.number; |
||||||
|
|
||||||
|
embedUrl = `${twoEmbedBase}/embed/tmdb/tv?id=${media.tmdbId}&s=${seasonNumber}&e=${episodeNumber}`; |
||||||
|
} |
||||||
|
|
||||||
|
const embed = await proxiedFetch<any>(embedUrl); |
||||||
|
progress(20); |
||||||
|
|
||||||
|
const embedPage = new DOMParser().parseFromString(embed, "text/html"); |
||||||
|
|
||||||
|
const pageServerItems = Array.from( |
||||||
|
embedPage.querySelectorAll(".item-server") |
||||||
|
); |
||||||
|
const pageStreamItem = pageServerItems.find((item) => |
||||||
|
item.textContent?.includes("Vidcloud") |
||||||
|
); |
||||||
|
|
||||||
|
const sourceId = pageStreamItem |
||||||
|
? pageStreamItem.getAttribute("data-id") |
||||||
|
: null; |
||||||
|
if (!sourceId) throw new Error("Unable to get source id"); |
||||||
|
|
||||||
|
const siteKey = embedPage |
||||||
|
.querySelector("body") |
||||||
|
?.getAttribute("data-recaptcha-key"); |
||||||
|
if (!siteKey) throw new Error("Unable to get site key"); |
||||||
|
|
||||||
|
const captchaToken = await fetchCaptchaToken(siteKey); |
||||||
|
if (!captchaToken) throw new Error("Unable to fetch captcha token"); |
||||||
|
progress(35); |
||||||
|
|
||||||
|
const stream = await fetchStream(sourceId, captchaToken); |
||||||
|
if (!stream.url) throw new Error("Unable to find stream url"); |
||||||
|
|
||||||
|
return { |
||||||
|
embeds: [], |
||||||
|
stream: { |
||||||
|
streamUrl: stream.url, |
||||||
|
quality: MWStreamQuality.QUNKNOWN, |
||||||
|
type: MWStreamType.MP4, |
||||||
|
captions: stream.subtitles.map((sub) => { |
||||||
|
return { |
||||||
|
langIso: sub.lang, |
||||||
|
url: `https://cc.2cdns.com${new URL(sub.url).pathname}`, |
||||||
|
type: MWCaptionType.VTT, |
||||||
|
}; |
||||||
|
}), |
||||||
|
}, |
||||||
|
}; |
||||||
|
}, |
||||||
|
}); |
||||||
Loading…
Reference in new issue