-
-
Notifications
You must be signed in to change notification settings - Fork 135
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
d735787
commit c634f6e
Showing
1 changed file
with
245 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<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, "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; |