diff --git a/src/extractors/megacloud.ts b/src/extractors/megacloud.ts new file mode 100644 index 0000000..88b6d94 --- /dev/null +++ b/src/extractors/megacloud.ts @@ -0,0 +1,245 @@ +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 { + 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( + 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, "MEGACLOUD"); + 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, sourceName: string) { + // extract needed variables + let allvars; + if (sourceName !== "MEGACLOUD") { + allvars = + text + .match( + /const (?:\w{1,2}=(?:'.{0,50}?'|\w{1,2}\(.{0,20}?\)).{0,20}?,){7}.+?;/gm + ) + ?.at(-1) ?? ""; + } else { + allvars = + text + .match(/const \w{1,2}=new URLSearchParams.+?;(?=function)/gm) + ?.at(-1) ?? ""; + } + // and convert their values into an array of numbers + const vars = allvars + .slice(0, -1) + .split("=") + .slice(1) + .map((pair) => Number(pair.split(",").at(0))) + .filter((num) => num === 0 || num); + + return vars; + } + + getSecret(encryptedString: string, values: number[]) { + let secret = "", + encryptedSource = encryptedString, + totalInc = 0; + + for (let i = 0; i < values[0]!; i++) { + let start, inc; + switch (i) { + case 0: + (start = values[2]), (inc = values[1]); + break; + case 1: + (start = values[4]), (inc = values[3]); + break; + case 2: + (start = values[6]), (inc = values[5]); + break; + case 3: + (start = values[8]), (inc = values[7]); + break; + case 4: + (start = values[10]), (inc = values[9]); + break; + case 5: + (start = values[12]), (inc = values[11]); + break; + case 6: + (start = values[14]), (inc = values[13]); + break; + case 7: + (start = values[16]), (inc = values[15]); + break; + case 8: + (start = values[18]), (inc = values[17]); + } + const from = start! + totalInc, + to = from + inc!; + (secret += encryptedString.slice(from, to)), + (encryptedSource = encryptedSource.replace( + encryptedString.substring(from, to), + "" + )), + (totalInc += inc!); + } + + 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; + } +} + +export default MegaCloud;