package main import ( "bytes" "crypto/aes" "crypto/cipher" "encoding/base64" "encoding/hex" "errors" "fmt" "github.com/goccy/go-json" "github.com/gocolly/colly" "github.com/rs/zerolog" "io" "net/http" "net/url" "regexp" "strconv" "strings" hibikeonlinestream "seanime/internal/extension/hibike/onlinestream" ) const ( DefaultServer = "default" GogoanimeProvider = "gogoanime-external" GogocdnServer = "gogocdn" VidstreamingServer = "vidstreaming" StreamSBServer = "streamsb" ) type Gogoanime struct { BaseURL string AjaxURL string Client http.Client UserAgent string logger *zerolog.Logger } func NewProvider(logger *zerolog.Logger) hibikeonlinestream.Provider { return &Gogoanime{ BaseURL: "https://anitaku.to", AjaxURL: "https://ajax.gogocdn.net", Client: http.Client{}, UserAgent: util.GetRandomUserAgent(), logger: logger, } } func (g *Gogoanime) GetEpisodeServers() []string { return []string{GogocdnServer, VidstreamingServer} } func (g *Gogoanime) Search(query string, dubbed bool) ([]*hibikeonlinestream.SearchResult, error) { var results []*hibikeonlinestream.SearchResult g.logger.Debug().Str("query", query).Bool("dubbed", dubbed).Msg("gogoanime: Searching anime") c := colly.NewCollector( colly.UserAgent(g.UserAgent), ) c.OnHTML(".last_episodes > ul > li", func(e *colly.HTMLElement) { id := "" idParts := strings.Split(e.ChildAttr("p.name > a", "href"), "/") if len(idParts) > 2 { id = idParts[2] } title := e.ChildText("p.name > a") url := g.BaseURL + e.ChildAttr("p.name > a", "href") subOrDub := hibikeonlinestream.Sub if strings.Contains(strings.ToLower(e.ChildText("p.name > a")), "dub") { subOrDub = hibikeonlinestream.Dub } results = append(results, &hibikeonlinestream.SearchResult{ ID: id, Title: title, URL: url, SubOrDub: subOrDub, }) }) searchURL := g.BaseURL + "/search.html?keyword=" + url.QueryEscape(query) if dubbed { searchURL += "%20(Dub)" } err := c.Visit(searchURL) if err != nil { return nil, err } g.logger.Debug().Int("count", len(results)).Msg("gogoanime: Fetched anime") return results, nil } func (g *Gogoanime) FindEpisode(id string) ([]*hibikeonlinestream.EpisodeDetails, error) { var episodes []*hibikeonlinestream.EpisodeDetails g.logger.Debug().Str("id", id).Msg("gogoanime: Fetching episodes") if !strings.Contains(id, "gogoanime") { id = fmt.Sprintf("%s/category/%s", g.BaseURL, id) } c := colly.NewCollector( colly.UserAgent(g.UserAgent), ) var epStart, epEnd, movieID, alias string c.OnHTML("#episode_page > li > a", func(e *colly.HTMLElement) { if epStart == "" { epStart = e.Attr("ep_start") } epEnd = e.Attr("ep_end") }) c.OnHTML("#movie_id", func(e *colly.HTMLElement) { movieID = e.Attr("value") }) c.OnHTML("#alias", func(e *colly.HTMLElement) { alias = e.Attr("value") }) err := c.Visit(id) if err != nil { g.logger.Error().Err(err).Msg("gogoanime: Failed to fetch episodes") return nil, err } c2 := colly.NewCollector( colly.UserAgent(g.UserAgent), ) c2.OnHTML("#episode_related > li", func(e *colly.HTMLElement) { episodeIDParts := strings.Split(e.ChildAttr("a", "href"), "/") if len(episodeIDParts) < 2 { return } episodeID := strings.TrimSpace(episodeIDParts[1]) episodeNumberStr := strings.TrimPrefix(e.ChildText("div.name"), "EP ") episodeNumber, err := strconv.Atoi(episodeNumberStr) if err != nil { g.logger.Error().Err(err).Str("episodeID", episodeID).Msg("failed to parse episode number") return } episodes = append(episodes, &hibikeonlinestream.EpisodeDetails{ Provider: GogoanimeProvider, ID: episodeID, Number: episodeNumber, URL: g.BaseURL + "/" + episodeID, }) }) ajaxURL := fmt.Sprintf("%s/ajax/load-list-episode", g.AjaxURL) ajaxParams := url.Values{ "ep_start": {epStart}, "ep_end": {epEnd}, "id": {movieID}, "alias": {alias}, "default_ep": {"0"}, } ajaxURLWithParams := fmt.Sprintf("%s?%s", ajaxURL, ajaxParams.Encode()) err = c2.Visit(ajaxURLWithParams) if err != nil { g.logger.Error().Err(err).Msg("gogoanime: Failed to fetch episodes") return nil, err } g.logger.Debug().Int("count", len(episodes)).Msg("gogoanime: Fetched episodes") return episodes, nil } func (g *Gogoanime) FindEpisodeServer(episodeInfo *hibikeonlinestream.EpisodeDetails, server string) (*hibikeonlinestream.EpisodeServer, error) { var source *hibikeonlinestream.EpisodeServer if server == DefaultServer { server = GogocdnServer } g.logger.Debug().Str("server", string(server)).Str("episodeID", episodeInfo.ID).Msg("gogoanime: Fetching server sources") c := colly.NewCollector() switch server { case VidstreamingServer: c.OnHTML(".anime_muti_link > ul > li.vidcdn > a", func(e *colly.HTMLElement) { src := e.Attr("data-video") gogocdn := NewGogoCDN() videoSources, err := gogocdn.Extract(src) if err == nil { source = &hibikeonlinestream.EpisodeServer{ Provider: GogoanimeProvider, Server: server, Headers: map[string]string{ "Referer": g.BaseURL + "/" + episodeInfo.ID, }, VideoSources: videoSources, } } }) case GogocdnServer, "": c.OnHTML("#load_anime > div > div > iframe", func(e *colly.HTMLElement) { src := e.Attr("src") gogocdn := NewGogoCDN() videoSources, err := gogocdn.Extract(src) if err == nil { source = &hibikeonlinestream.EpisodeServer{ Provider: GogoanimeProvider, Server: server, Headers: map[string]string{ "Referer": g.BaseURL + "/" + episodeInfo.ID, }, VideoSources: videoSources, } } }) case StreamSBServer: c.OnHTML(".anime_muti_link > ul > li.streamsb > a", func(e *colly.HTMLElement) { src := e.Attr("data-video") streamsb := NewStreamSB() videoSources, err := streamsb.Extract(src) if err == nil { source = &hibikeonlinestream.EpisodeServer{ Provider: GogoanimeProvider, Server: server, Headers: map[string]string{ "Referer": g.BaseURL + "/" + episodeInfo.ID, "watchsb": "streamsb", "User-Agent": g.UserAgent, }, VideoSources: videoSources, } } }) } err := c.Visit(g.BaseURL + "/" + episodeInfo.ID) if err != nil { return nil, err } if source == nil { g.logger.Warn().Str("server", string(server)).Msg("gogoanime: No sources found") return nil, fmt.Errorf("no sources found") } g.logger.Debug().Str("server", string(server)).Int("videoSources", len(source.VideoSources)).Msg("gogoanime: Fetched server sources") return source, nil } type cdnKeys struct { key []byte secondKey []byte iv []byte } type GogoCDN struct { client *http.Client serverName string keys cdnKeys referrer string } func NewGogoCDN() *GogoCDN { return &GogoCDN{ client: &http.Client{}, serverName: "goload", keys: cdnKeys{ key: []byte("37911490979715163134003223491201"), secondKey: []byte("54674138327930866480207815084989"), iv: []byte("3134003223491201"), }, } } // Extract fetches and extracts video sources from the provided URI. func (g *GogoCDN) Extract(uri string) (vs []*hibikeonlinestream.VideoSource, err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("failed to extract video sources") } }() // Instantiate a new collector c := colly.NewCollector( // Allow visiting the same page multiple times colly.AllowURLRevisit(), ) ur, err := url.Parse(uri) if err != nil { return nil, err } // Variables to hold extracted values var scriptValue, id string id = ur.Query().Get("id") // Find and extract the script value and id c.OnHTML("script[data-name='episode']", func(e *colly.HTMLElement) { scriptValue = e.Attr("data-value") }) // Start scraping err = c.Visit(uri) if err != nil { return nil, err } // Check if scriptValue and id are found if scriptValue == "" || id == "" { return nil, errors.New("script value or id not found") } // Extract video sources ajaxUrl := fmt.Sprintf("%s://%s/encrypt-ajax.php?%s", ur.Scheme, ur.Host, g.generateEncryptedAjaxParams(id, scriptValue)) req, err := http.NewRequest("GET", ajaxUrl, nil) if err != nil { return nil, err } req.Header.Set("X-Requested-With", "XMLHttpRequest") req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36") req.Header.Set("Accept", "application/json, text/javascript, */*; q=0.01") encryptedData, err := g.client.Do(req) if err != nil { return nil, err } defer encryptedData.Body.Close() encryptedDataBytesRes, err := io.ReadAll(encryptedData.Body) if err != nil { return nil, err } var encryptedDataBytes map[string]string err = json.Unmarshal(encryptedDataBytesRes, &encryptedDataBytes) if err != nil { return nil, err } data, err := g.decryptAjaxData(encryptedDataBytes["data"]) source, ok := data["source"].([]interface{}) // Check if source is found if !ok { return nil, errors.New("source not found") } var results []*hibikeonlinestream.VideoSource urls := make([]string, 0) for _, src := range source { s := src.(map[string]interface{}) urls = append(urls, s["file"].(string)) } sourceBK, ok := data["source_bk"].([]interface{}) if ok { for _, src := range sourceBK { s := src.(map[string]interface{}) urls = append(urls, s["file"].(string)) } } for _, url := range urls { vs, ok := g.urlToVideoSource(url, source, sourceBK) if ok { results = append(results, vs...) } } return results, nil } func (g *GogoCDN) urlToVideoSource(url string, source []interface{}, sourceBK []interface{}) (vs []*hibikeonlinestream.VideoSource, ok bool) { defer func() { if r := recover(); r != nil { ok = false } }() ret := make([]*hibikeonlinestream.VideoSource, 0) if strings.Contains(url, ".m3u8") { resResult, err := http.Get(url) if err != nil { return nil, false } defer resResult.Body.Close() bodyBytes, err := io.ReadAll(resResult.Body) if err != nil { return nil, false } bodyString := string(bodyBytes) resolutions := regexp.MustCompile(`(RESOLUTION=)(.*)(\s*?)(\s.*)`).FindAllStringSubmatch(bodyString, -1) baseURL := url[:strings.LastIndex(url, "/")] for _, res := range resolutions { quality := strings.Split(strings.Split(res[2], "x")[1], ",")[0] url := fmt.Sprintf("%s/%s", baseURL, strings.TrimSpace(res[4])) ret = append(ret, &hibikeonlinestream.VideoSource{URL: url, Type: hibikeonlinestream.VideoSourceM3U8, Quality: quality + "p"}) } ret = append(ret, &hibikeonlinestream.VideoSource{URL: url, Type: hibikeonlinestream.VideoSourceM3U8, Quality: "default"}) } else { for _, src := range source { s := src.(map[string]interface{}) if s["file"].(string) == url { quality := strings.Split(s["label"].(string), " ")[0] + "p" ret = append(ret, &hibikeonlinestream.VideoSource{URL: url, Type: hibikeonlinestream.VideoSourceMP4, Quality: quality}) } } if sourceBK != nil { for _, src := range sourceBK { s := src.(map[string]interface{}) if s["file"].(string) == url { ret = append(ret, &hibikeonlinestream.VideoSource{URL: url, Type: hibikeonlinestream.VideoSourceMP4, Quality: "backup"}) } } } } return ret, true } // generateEncryptedAjaxParams generates encrypted AJAX parameters. func (g *GogoCDN) generateEncryptedAjaxParams(id, scriptValue string) string { encryptedKey := g.encrypt(id, g.keys.iv, g.keys.key) decryptedToken := g.decrypt(scriptValue, g.keys.iv, g.keys.key) return fmt.Sprintf("id=%s&alias=%s", encryptedKey, decryptedToken) } // encrypt encrypts the given text using AES CBC mode. func (g *GogoCDN) encrypt(text string, iv []byte, key []byte) string { block, _ := aes.NewCipher(key) textBytes := []byte(text) textBytes = pkcs7Padding(textBytes, aes.BlockSize) cipherText := make([]byte, len(textBytes)) mode := cipher.NewCBCEncrypter(block, iv) mode.CryptBlocks(cipherText, textBytes) return base64.StdEncoding.EncodeToString(cipherText) } // decrypt decrypts the given text using AES CBC mode. func (g *GogoCDN) decrypt(text string, iv []byte, key []byte) string { block, _ := aes.NewCipher(key) cipherText, _ := base64.StdEncoding.DecodeString(text) plainText := make([]byte, len(cipherText)) mode := cipher.NewCBCDecrypter(block, iv) mode.CryptBlocks(plainText, cipherText) plainText = pkcs7Trimming(plainText) return string(plainText) } func (g *GogoCDN) decryptAjaxData(encryptedData string) (map[string]interface{}, error) { decodedData, err := base64.StdEncoding.DecodeString(encryptedData) if err != nil { return nil, err } block, err := aes.NewCipher(g.keys.secondKey) if err != nil { return nil, err } if len(decodedData) < aes.BlockSize { return nil, fmt.Errorf("cipher text too short") } iv := g.keys.iv mode := cipher.NewCBCDecrypter(block, iv) mode.CryptBlocks(decodedData, decodedData) // Remove padding decodedData = pkcs7Trimming(decodedData) var data map[string]interface{} err = json.Unmarshal(decodedData, &data) if err != nil { return nil, err } return data, nil } // pkcs7Padding pads the text to be a multiple of blockSize using Pkcs7 padding. func pkcs7Padding(text []byte, blockSize int) []byte { padding := blockSize - len(text)%blockSize padText := bytes.Repeat([]byte{byte(padding)}, padding) return append(text, padText...) } // pkcs7Trimming removes Pkcs7 padding from the text. func pkcs7Trimming(text []byte) []byte { length := len(text) unpadding := int(text[length-1]) return text[:(length - unpadding)] } type StreamSB struct { Host string Host2 string UserAgent string } func NewStreamSB() *StreamSB { return &StreamSB{ Host: "https://streamsss.net/sources50", Host2: "https://watchsb.com/sources50", UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36", } } func (s *StreamSB) Payload(hex string) string { return "566d337678566f743674494a7c7c" + hex + "7c7c346b6767586d6934774855537c7c73747265616d7362/6565417268755339773461447c7c346133383438333436313335376136323337373433383634376337633465366534393338373136643732373736343735373237613763376334363733353737303533366236333463353333363534366137633763373337343732363536313664373336327c7c6b586c3163614468645a47617c7c73747265616d7362" } func (s *StreamSB) Extract(uri string) (vs []*hibikeonlinestream.VideoSource, err error) { defer func() { if r := recover(); r != nil { err = errors.New("failed to extract video sources") } }() var ret []*hibikeonlinestream.VideoSource id := strings.Split(uri, "/e/")[1] if strings.Contains(id, "html") { id = strings.Split(id, ".html")[0] } if id == "" { return nil, errors.New("cannot find ID") } client := &http.Client{} req, _ := http.NewRequest("GET", fmt.Sprintf("%s/%s", s.Host, s.Payload(hex.EncodeToString([]byte(id)))), nil) req.Header.Add("watchsb", "sbstream") req.Header.Add("User-Agent", s.UserAgent) req.Header.Add("Referer", uri) res, err := client.Do(req) if err != nil { return nil, err } defer res.Body.Close() body, _ := io.ReadAll(res.Body) var jsonResponse map[string]interface{} err = json.Unmarshal(body, &jsonResponse) if err != nil { return nil, err } streamData, ok := jsonResponse["stream_data"].(map[string]interface{}) if !ok { return nil, fmt.Errorf("stream data not found") } m3u8Urls, err := client.Get(streamData["file"].(string)) if err != nil { return nil, err } defer m3u8Urls.Body.Close() m3u8Body, err := io.ReadAll(m3u8Urls.Body) if err != nil { return nil, err } videoList := strings.Split(string(m3u8Body), "#EXT-X-STREAM-INF:") for _, video := range videoList { if !strings.Contains(video, "m3u8") { continue } url := strings.Split(video, "\n")[1] quality := strings.Split(strings.Split(video, "RESOLUTION=")[1], ",")[0] quality = strings.Split(quality, "x")[1] ret = append(ret, &hibikeonlinestream.VideoSource{ URL: url, Quality: quality + "p", Type: hibikeonlinestream.VideoSourceM3U8, }) } ret = append(ret, &hibikeonlinestream.VideoSource{ URL: streamData["file"].(string), Quality: "auto", Type: map[bool]hibikeonlinestream.VideoSourceType{true: hibikeonlinestream.VideoSourceM3U8, false: hibikeonlinestream.VideoSourceMP4}[strings.Contains(streamData["file"].(string), ".m3u8")], }) return ret, nil }