Skip to content

Commit

Permalink
feat: add scraper extractors
Browse files Browse the repository at this point in the history
  • Loading branch information
ghoshRitesh12 committed Aug 5, 2024
1 parent 9c9d781 commit fa01d83
Show file tree
Hide file tree
Showing 5 changed files with 519 additions and 0 deletions.
6 changes: 6 additions & 0 deletions src/extractors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import StreamSB from "./streamsb.js";
import StreamTape from "./streamtape.js";
import RapidCloud from "./rapidcloud.js";
import MegaCloud from "./megacloud.js";

export { StreamSB, StreamTape, RapidCloud, MegaCloud };
227 changes: 227 additions & 0 deletions src/extractors/megacloud.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
import axios from "axios";
import crypto from "crypto";
import createHttpError from "http-errors";

// https://megacloud.tv/embed-2/e-1/dBqCr5BcOhnD?k=1

const megacloud = {
script: "https://megacloud.tv/js/player/a/prod/e1-player.min.js?v=",
sources: "https://megacloud.tv/embed-2/ajax/e-1/getSources?id=",
} as const;

type track = {
file: string;
kind: string;
label?: string;
default?: boolean;
};

type intro_outro = {
start: number;
end: number;
};

type unencryptedSrc = {
file: string;
type: string;
};

type extractedSrc = {
sources: string | unencryptedSrc[];
tracks: track[];
encrypted: boolean;
intro: intro_outro;
outro: intro_outro;
server: number;
};

interface ExtractedData
extends Pick<extractedSrc, "intro" | "outro" | "tracks"> {
sources: { url: string; type: string }[];
}

class MegaCloud {
// private serverName = "megacloud";

async extract(videoUrl: URL) {
try {
const extractedData: ExtractedData = {
tracks: [],
intro: {
start: 0,
end: 0,
},
outro: {
start: 0,
end: 0,
},
sources: [],
};

const videoId = videoUrl?.href?.split("/")?.pop()?.split("?")[0];
const { data: srcsData } = await axios.get<extractedSrc>(
megacloud.sources.concat(videoId || ""),
{
headers: {
Accept: "*/*",
"X-Requested-With": "XMLHttpRequest",
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
Referer: videoUrl.href,
},
}
);
if (!srcsData) {
throw createHttpError.NotFound("Url may have an invalid video id");
}

// console.log(JSON.stringify(srcsData, null, 2));

const encryptedString = srcsData.sources;
if (!srcsData.encrypted && Array.isArray(encryptedString)) {
extractedData.intro = srcsData.intro;
extractedData.outro = srcsData.outro;
extractedData.tracks = srcsData.tracks;
extractedData.sources = encryptedString.map((s) => ({
url: s.file,
type: s.type,
}));

return extractedData;
}

let text: string;
const { data } = await axios.get(
megacloud.script.concat(Date.now().toString())
);

text = data;
if (!text) {
throw createHttpError.InternalServerError(
"Couldn't fetch script to decrypt resource"
);
}

const vars = this.extractVariables(text);
if (!vars.length) {
throw new Error(
"Can't find variables. Perhaps the extractor is outdated."
);
}

const { secret, encryptedSource } = this.getSecret(
encryptedString as string,
vars
);
const decrypted = this.decrypt(encryptedSource, secret);
try {
const sources = JSON.parse(decrypted);
extractedData.intro = srcsData.intro;
extractedData.outro = srcsData.outro;
extractedData.tracks = srcsData.tracks;
extractedData.sources = sources.map((s: any) => ({
url: s.file,
type: s.type,
}));

return extractedData;
} catch (error) {
throw createHttpError.InternalServerError("Failed to decrypt resource");
}
} catch (err) {
// console.log(err);
throw err;
}
}

extractVariables(text: string) {
// copied from github issue #30 'https://github.com/ghoshRitesh12/aniwatch-api/issues/30'
const regex =
/case\s*0x[0-9a-f]+:(?![^;]*=partKey)\s*\w+\s*=\s*(\w+)\s*,\s*\w+\s*=\s*(\w+);/g;
const matches = text.matchAll(regex);
const vars = Array.from(matches, (match) => {
const matchKey1 = this.matchingKey(match[1], text);
const matchKey2 = this.matchingKey(match[2], text);
try {
return [parseInt(matchKey1, 16), parseInt(matchKey2, 16)];
} catch (e) {
return [];
}
}).filter((pair) => pair.length > 0);

return vars;
}

getSecret(encryptedString: string, values: number[][]) {
let secret = "",
encryptedSource = "",
encryptedSourceArray = encryptedString.split(""),
currentIndex = 0;

for (const index of values) {
const start = index[0] + currentIndex;
const end = start + index[1];

for (let i = start; i < end; i++) {
secret += encryptedString[i];
encryptedSourceArray[i] = "";
}
currentIndex += index[1];
}

encryptedSource = encryptedSourceArray.join("");

return { secret, encryptedSource };
}

decrypt(encrypted: string, keyOrSecret: string, maybe_iv?: string) {
let key;
let iv;
let contents;
if (maybe_iv) {
key = keyOrSecret;
iv = maybe_iv;
contents = encrypted;
} else {
// copied from 'https://github.com/brix/crypto-js/issues/468'
const cypher = Buffer.from(encrypted, "base64");
const salt = cypher.subarray(8, 16);
const password = Buffer.concat([
Buffer.from(keyOrSecret, "binary"),
salt,
]);
const md5Hashes = [];
let digest = password;
for (let i = 0; i < 3; i++) {
md5Hashes[i] = crypto.createHash("md5").update(digest).digest();
digest = Buffer.concat([md5Hashes[i], password]);
}
key = Buffer.concat([md5Hashes[0], md5Hashes[1]]);
iv = md5Hashes[2];
contents = cypher.subarray(16);
}

const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv);
const decrypted =
decipher.update(
contents as any,
typeof contents === "string" ? "base64" : undefined,
"utf8"
) + decipher.final();

return decrypted;
}

// function copied from github issue #30 'https://github.com/ghoshRitesh12/aniwatch-api/issues/30'
matchingKey(value: string, script: string) {
const regex = new RegExp(`,${value}=((?:0x)?([0-9a-fA-F]+))`);
const match = script.match(regex);
if (match) {
return match[1].replace(/^0x/, "");
} else {
throw new Error("Failed to match the key");
}
}
}

export default MegaCloud;
Loading

0 comments on commit fa01d83

Please sign in to comment.