mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-27 09:31:50 +01:00
commit
61b75da402
@ -1,4 +1,4 @@
|
|||||||
import { ofetch } from "ofetch";
|
import { FetchOptions, FetchResponse, ofetch } from "ofetch";
|
||||||
|
|
||||||
import { conf } from "@/setup/config";
|
import { conf } from "@/setup/config";
|
||||||
|
|
||||||
@ -59,3 +59,36 @@ export function proxiedFetch<T>(url: string, ops: P<T>[1] = {}): R<T> {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function rawProxiedFetch<T>(
|
||||||
|
url: string,
|
||||||
|
ops: FetchOptions = {}
|
||||||
|
): Promise<FetchResponse<T>> {
|
||||||
|
let combinedUrl = ops?.baseURL ?? "";
|
||||||
|
if (
|
||||||
|
combinedUrl.length > 0 &&
|
||||||
|
combinedUrl.endsWith("/") &&
|
||||||
|
url.startsWith("/")
|
||||||
|
)
|
||||||
|
combinedUrl += url.slice(1);
|
||||||
|
else if (
|
||||||
|
combinedUrl.length > 0 &&
|
||||||
|
!combinedUrl.endsWith("/") &&
|
||||||
|
!url.startsWith("/")
|
||||||
|
)
|
||||||
|
combinedUrl += `/${url}`;
|
||||||
|
else combinedUrl += url;
|
||||||
|
|
||||||
|
const parsedUrl = new URL(combinedUrl);
|
||||||
|
Object.entries(ops?.params ?? {}).forEach(([k, v]) => {
|
||||||
|
parsedUrl.searchParams.set(k, v);
|
||||||
|
});
|
||||||
|
|
||||||
|
return baseFetch.raw(getProxyUrl(), {
|
||||||
|
...ops,
|
||||||
|
baseURL: undefined,
|
||||||
|
params: {
|
||||||
|
destination: parsedUrl.toString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@ -7,6 +7,7 @@ import "./providers/superstream";
|
|||||||
import "./providers/netfilm";
|
import "./providers/netfilm";
|
||||||
import "./providers/m4ufree";
|
import "./providers/m4ufree";
|
||||||
import "./providers/hdwatched";
|
import "./providers/hdwatched";
|
||||||
|
import "./providers/2embed";
|
||||||
|
|
||||||
// embeds
|
// embeds
|
||||||
import "./embeds/streamm4u";
|
import "./embeds/streamm4u";
|
||||||
|
251
src/backend/providers/2embed.ts
Normal file
251
src/backend/providers/2embed.ts
Normal file
@ -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
Block a user