A small web app for watching movies and shows easily
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

211 lines
6.0 KiB

import Base64 from "crypto-js/enc-base64";
import Utf8 from "crypto-js/enc-utf8";
import { MWEmbedType } from "@/backend/helpers/embed";
import { proxiedFetch } from "@/backend/helpers/fetch";
import { registerEmbedScraper } from "@/backend/helpers/register";
import {
MWCaptionType,
MWStreamQuality,
MWStreamType,
} from "@/backend/helpers/streams";
const qualityOrder = [
MWStreamQuality.Q1080P,
MWStreamQuality.Q720P,
MWStreamQuality.Q480P,
MWStreamQuality.Q360P,
];
async function fetchCaptchaToken(domain: string, recaptchaKey: string) {
const domainHash = Base64.stringify(Utf8.parse(domain)).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: domain,
};
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;
}
registerEmbedScraper({
id: "streamsb",
displayName: "StreamSB",
for: MWEmbedType.STREAMSB,
rank: 150,
async getStream({ url, progress }) {
/* Url variations
- domain.com/{id}?.html
- domain.com/{id}
- domain.com/embed-{id}
- domain.com/d/{id}
- domain.com/e/{id}
- domain.com/e/{id}-embed
*/
const streamsbUrl = url
.replace(".html", "")
.replace("embed-", "")
.replace("e/", "")
.replace("d/", "");
const parsedUrl = new URL(streamsbUrl);
const base = await proxiedFetch<any>(
`${parsedUrl.origin}/d${parsedUrl.pathname}`
);
progress(20);
// Parse captions from url
const captionUrl = parsedUrl.searchParams.get("caption_1");
const captionLang = parsedUrl.searchParams.get("sub_1");
const basePage = new DOMParser().parseFromString(base, "text/html");
const downloadVideoFunctions = basePage.querySelectorAll(
"[onclick^=download_video]"
);
let dlDetails = [];
for (const func of downloadVideoFunctions) {
const funcContents = func.getAttribute("onclick");
const regExpFunc = /download_video\('(.+?)','(.+?)','(.+?)'\)/;
const matchesFunc = regExpFunc.exec(funcContents ?? "");
if (matchesFunc !== null) {
const quality = func.querySelector("span")?.textContent;
const regExpQuality = /(.+?) \((.+?)\)/;
const matchesQuality = regExpQuality.exec(quality ?? "");
if (matchesQuality !== null) {
dlDetails.push({
parameters: [matchesFunc[1], matchesFunc[2], matchesFunc[3]],
quality: {
label: matchesQuality[1].trim(),
size: matchesQuality[2],
},
});
}
}
}
dlDetails = dlDetails.sort((a, b) => {
const aQuality = qualityOrder.indexOf(a.quality.label as MWStreamQuality);
const bQuality = qualityOrder.indexOf(b.quality.label as MWStreamQuality);
return aQuality - bQuality;
});
progress(40);
let dls = await Promise.all(
dlDetails.map(async (dl) => {
const getDownload = await proxiedFetch<any>(
`/dl?op=download_orig&id=${dl.parameters[0]}&mode=${dl.parameters[1]}&hash=${dl.parameters[2]}`,
{
baseURL: parsedUrl.origin,
}
);
const downloadPage = new DOMParser().parseFromString(
getDownload,
"text/html"
);
const recaptchaKey = downloadPage
.querySelector(".g-recaptcha")
?.getAttribute("data-sitekey");
if (!recaptchaKey) throw new Error("Unable to get captcha key");
const captchaToken = await fetchCaptchaToken(
parsedUrl.origin,
recaptchaKey
);
if (!captchaToken) throw new Error("Unable to get captcha token");
const dlForm = new FormData();
dlForm.append("op", "download_orig");
dlForm.append("id", dl.parameters[0]);
dlForm.append("mode", dl.parameters[1]);
dlForm.append("hash", dl.parameters[2]);
dlForm.append("g-recaptcha-response", captchaToken);
const download = await proxiedFetch<any>(
`/dl?op=download_orig&id=${dl.parameters[0]}&mode=${dl.parameters[1]}&hash=${dl.parameters[2]}`,
{
baseURL: parsedUrl.origin,
method: "POST",
body: dlForm,
}
);
const dlLink = new DOMParser()
.parseFromString(download, "text/html")
.querySelector(".btn.btn-light.btn-lg")
?.getAttribute("href");
return {
quality: dl.quality.label as MWStreamQuality,
url: dlLink,
size: dl.quality.size,
captions:
captionUrl && captionLang
? [
{
url: captionUrl,
langIso: captionLang,
type: MWCaptionType.VTT,
},
]
: [],
};
})
);
dls = dls.filter((d) => !!d.url);
progress(60);
// TODO: Quality selection for embed scrapers
const dl = dls[0];
if (!dl.url) throw new Error("No stream url found");
return {
embedId: MWEmbedType.STREAMSB,
streamUrl: dl.url,
quality: dl.quality,
captions: dl.captions,
type: MWStreamType.MP4,
};
},
});