package onlinestream_sources import ( "crypto/aes" "crypto/cipher" "crypto/md5" "encoding/base64" "encoding/json" "errors" "io" "net/http" "regexp" hibikeonlinestream "seanime/internal/extension/hibike/onlinestream" "seanime/internal/util" "strconv" "strings" ) type MegaCloud struct { Script string Sources string UserAgent string } func NewMegaCloud() *MegaCloud { return &MegaCloud{ Script: "https://megacloud.tv/js/player/a/prod/e1-player.min.js", Sources: "https://megacloud.tv/embed-2/ajax/e-1/getSources?id=", UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3", } } func (m *MegaCloud) Extract(uri string) (vs []*hibikeonlinestream.VideoSource, err error) { defer util.HandlePanicInModuleThen("onlinestream/sources/megacloud/Extract", func() { err = ErrVideoSourceExtraction }) videoIdParts := strings.Split(uri, "/") videoId := videoIdParts[len(videoIdParts)-1] videoId = strings.Split(videoId, "?")[0] client := &http.Client{} req, err := http.NewRequest("GET", m.Sources+videoId, nil) if err != nil { return nil, err } req.Header.Set("Accept", "*/*") req.Header.Set("X-Requested-With", "XMLHttpRequest") req.Header.Set("User-Agent", m.UserAgent) req.Header.Set("Referer", uri) res, err := client.Do(req) if err != nil { return nil, err } defer res.Body.Close() var srcData map[string]interface{} err = json.NewDecoder(res.Body).Decode(&srcData) if err != nil { return nil, err } subtitles := make([]*hibikeonlinestream.VideoSubtitle, 0) for idx, s := range srcData["tracks"].([]interface{}) { sub := s.(map[string]interface{}) label, ok := sub["label"].(string) if ok { subtitle := &hibikeonlinestream.VideoSubtitle{ URL: sub["file"].(string), ID: label, Language: label, IsDefault: idx == 0, } subtitles = append(subtitles, subtitle) } } if encryptedString, ok := srcData["sources"]; ok { switch encryptedString.(type) { case []interface{}: if len(encryptedString.([]interface{})) == 0 { return nil, ErrNoVideoSourceFound } videoSources := make([]*hibikeonlinestream.VideoSource, 0) if e, ok := encryptedString.([]interface{})[0].(map[string]interface{}); ok { file, ok := e["file"].(string) if ok { videoSources = append(videoSources, &hibikeonlinestream.VideoSource{ URL: file, Type: map[bool]hibikeonlinestream.VideoSourceType{true: hibikeonlinestream.VideoSourceM3U8, false: hibikeonlinestream.VideoSourceMP4}[strings.Contains(file, ".m3u8")], Subtitles: subtitles, Quality: QualityAuto, }) } } if len(videoSources) == 0 { return nil, ErrNoVideoSourceFound } return videoSources, nil case []map[string]interface{}: if srcData["encrypted"].(bool) && ok { videoSources := make([]*hibikeonlinestream.VideoSource, 0) for _, e := range encryptedString.([]map[string]interface{}) { videoSources = append(videoSources, &hibikeonlinestream.VideoSource{ URL: e["file"].(string), Type: map[bool]hibikeonlinestream.VideoSourceType{true: hibikeonlinestream.VideoSourceM3U8, false: hibikeonlinestream.VideoSourceMP4}[strings.Contains(e["file"].(string), ".m3u8")], Subtitles: subtitles, Quality: QualityAuto, }) } if len(videoSources) == 0 { return nil, ErrNoVideoSourceFound } return videoSources, nil } case string: res, err = client.Get(m.Script) if err != nil { return nil, err } defer res.Body.Close() text, err := io.ReadAll(res.Body) if err != nil { return nil, errors.New("couldn't fetch script to decrypt resource") } values, err := m.extractVariables(string(text)) if err != nil { return nil, err } secret, encryptedSource := m.getSecret(encryptedString.(string), values) //if err != nil { // return nil, err //} decrypted, err := m.decrypt(encryptedSource, secret) if err != nil { return nil, err } var decryptedData []map[string]interface{} err = json.Unmarshal([]byte(decrypted), &decryptedData) if err != nil { return nil, err } sources := make([]*hibikeonlinestream.VideoSource, 0) for _, e := range decryptedData { sources = append(sources, &hibikeonlinestream.VideoSource{ URL: e["file"].(string), Type: map[bool]hibikeonlinestream.VideoSourceType{true: hibikeonlinestream.VideoSourceM3U8, false: hibikeonlinestream.VideoSourceMP4}[strings.Contains(e["file"].(string), ".m3u8")], Subtitles: subtitles, Quality: QualityAuto, }) } if len(sources) == 0 { return nil, ErrNoVideoSourceFound } return sources, nil } } return nil, ErrNoVideoSourceFound } func (m *MegaCloud) extractVariables(text string) ([][]int, error) { re := regexp.MustCompile(`case\s*0x[0-9a-f]+:\s*\w+\s*=\s*(\w+)\s*,\s*\w+\s*=\s*(\w+);`) matches := re.FindAllStringSubmatch(text, -1) var vars [][]int for _, match := range matches { if len(match) < 3 { continue } caseLine := match[0] if strings.Contains(caseLine, "partKey") { continue } matchKey1, err1 := m.matchingKey(match[1], text) matchKey2, err2 := m.matchingKey(match[2], text) if err1 != nil || err2 != nil { continue } key1, err1 := strconv.ParseInt(matchKey1, 16, 64) key2, err2 := strconv.ParseInt(matchKey2, 16, 64) if err1 != nil || err2 != nil { continue } vars = append(vars, []int{int(key1), int(key2)}) } return vars, nil } func (m *MegaCloud) matchingKey(value, script string) (string, error) { regexPattern := `,` + regexp.QuoteMeta(value) + `=((?:0x)?([0-9a-fA-F]+))` re := regexp.MustCompile(regexPattern) match := re.FindStringSubmatch(script) if len(match) > 1 { return strings.TrimPrefix(match[1], "0x"), nil } return "", errors.New("failed to match the key") } func (m *MegaCloud) getSecret(encryptedString string, values [][]int) (string, string) { secret := "" encryptedSourceArray := strings.Split(encryptedString, "") currentIndex := 0 for _, index := range values { start := index[0] + currentIndex end := start + index[1] for i := start; i < end; i++ { secret += string(encryptedString[i]) encryptedSourceArray[i] = "" } currentIndex += index[1] } encryptedSource := strings.Join(encryptedSourceArray, "") return secret, encryptedSource } //func (m *MegaCloud) getSecret(encryptedString string, values []int) (string, string, error) { // var secret string // var encryptedSource = encryptedString // var totalInc int // // for i := 0; i < values[0]; i++ { // var start, inc int // // switch i { // case 0: // start = values[2] // inc = values[1] // case 1: // start = values[4] // inc = values[3] // case 2: // start = values[6] // inc = values[5] // case 3: // start = values[8] // inc = values[7] // case 4: // start = values[10] // inc = values[9] // case 5: // start = values[12] // inc = values[11] // case 6: // start = values[14] // inc = values[13] // case 7: // start = values[16] // inc = values[15] // case 8: // start = values[18] // inc = values[17] // default: // return "", "", errors.New("invalid index") // } // // from := start + totalInc // to := from + inc // // secret += encryptedString[from:to] // encryptedSource = strings.Replace(encryptedSource, encryptedString[from:to], "", 1) // totalInc += inc // } // // return secret, encryptedSource, nil //} func (m *MegaCloud) decrypt(encrypted, keyOrSecret string) (string, error) { cypher, err := base64.StdEncoding.DecodeString(encrypted) if err != nil { return "", err } salt := cypher[8:16] password := append([]byte(keyOrSecret), salt...) md5Hashes := make([][]byte, 3) digest := password for i := 0; i < 3; i++ { hash := md5.Sum(digest) md5Hashes[i] = hash[:] digest = append(hash[:], password...) } key := append(md5Hashes[0], md5Hashes[1]...) iv := md5Hashes[2] contents := cypher[16:] block, err := aes.NewCipher(key) if err != nil { return "", err } mode := cipher.NewCBCDecrypter(block, iv) mode.CryptBlocks(contents, contents) contents, err = pkcs7Unpad(contents, block.BlockSize()) if err != nil { return "", err } return string(contents), nil } func pkcs7Unpad(data []byte, blockSize int) ([]byte, error) { if blockSize <= 0 { return nil, errors.New("invalid blocksize") } if len(data)%blockSize != 0 || len(data) == 0 { return nil, errors.New("invalid PKCS7 data (block size must be a multiple of input length)") } padLen := int(data[len(data)-1]) if padLen > blockSize || padLen == 0 { return nil, errors.New("invalid PKCS7 padding") } for i := 0; i < padLen; i++ { if data[len(data)-1-i] != byte(padLen) { return nil, errors.New("invalid PKCS7 padding") } } return data[:len(data)-padLen], nil }