Newer
Older
anime-fetcher / main.go
package main

import (
	"encoding/json"
	"errors"
	"fmt"
	"github.com/PuerkitoBio/goquery"
	chi "github.com/go-chi/chi/v5"
	"github.com/go-chi/chi/v5/middleware"
	"github.com/google/uuid"
	"html"
	"io"
	"log"
	"net/http"
	"net/url"
	"os"
	"os/exec"
	"strings"
	"sync"
	"time"
)

func main() {
	m := &sync.Map{}

	r := chi.NewRouter()
	r.Use(middleware.Logger)
	r.Get("/", func(w http.ResponseWriter, r *http.Request) {
		file, err := os.ReadFile("frontend/index.html")
		if err != nil {
			panic(err)
		}
		w.Write(file)
	})
	r.Post("/success", func(w http.ResponseWriter, r *http.Request) {
		file, err := os.ReadFile("frontend/success.html")
		if err != nil {
			panic(err)
		}
		w.Write(file)
	})
	r.Post("/failed", func(w http.ResponseWriter, r *http.Request) {
		file, err := os.ReadFile("frontend/failed.html")
		if err != nil {
			panic(err)
		}
		w.Write(file)
	})

	r.Get("/session", func(w http.ResponseWriter, r *http.Request) {
		m1 := make(map[string]Request)

		m.Range(func(key, value any) bool {
			// Type assertion is required since Range returns 'any' types
			k := key.(string)
			v := value.(Request)

			m1[k] = v

			return true // Return true to continue iterating
		})

		data, err := json.Marshal(m1)
		if err != nil {
			http.Error(w, "Failed to submit request", http.StatusInternalServerError)
			return
		}

		_, err = w.Write(data)
		if err != nil {
			http.Error(w, "Failed to submit request", http.StatusInternalServerError)
			return
		}
		w.WriteHeader(http.StatusOK)
	})

	r.Post("/session", func(w http.ResponseWriter, r *http.Request) {
		if err := r.ParseForm(); err != nil {
			http.Error(w, "Failed to parse form", http.StatusBadRequest)
			return
		}

		// Retrieve input values by their HTML "name" attribute
		u := r.FormValue("url")
		name := r.FormValue("name")
		season := r.FormValue("season")

		UUID, err := uuid.NewUUID()
		if err != nil {
			http.Error(w, "Failed to submit request", http.StatusInternalServerError)
			return
		}

		m.Store(UUID.String(), Request{
			URL:    u,
			Name:   name,
			Season: season,
		})

		http.Redirect(w, r, "/success", http.StatusTemporaryRedirect)
	})

	go processRequest(m)
	http.ListenAndServe(":3000", r)
}

type Request struct {
	URL    string `json:"url"`
	Name   string `json:"name"`
	Season string `json:"season"`
}

func processRequest(m *sync.Map) {
	for {
		m.Range(func(key, value any) bool {
			// Type assertion is required since Range returns 'any' types
			//k := key.(int)
			v := value.(Request)

			Download(v)

			return true // Return true to continue iterating
		})

		m.Clear()
		time.Sleep(time.Second * 30)
	}
}

func Download(request Request) {
	seasonID, err := extractSeasonID(request.URL)
	if err != nil {
		log.Printf("%+v\n", err)
		return
	}

	episodeCount, err := getEpisodeList(seasonID)
	if err != nil {
		log.Printf("%+v\n", err)
		return
	}
	for i := 1; i < episodeCount+1; i++ {
		sourceID, err := getAniwaveSourceID(seasonID, i)
		if err != nil {
			log.Printf("%+v\n", err)
			return
		}

		sourceUrl, err := getAniwavesSource(request.URL, sourceID)
		if err != nil {
			log.Printf("%+v\n", err)
			return
		}

		playEchoVideoSourceeID, err := extractSourceID(sourceUrl)
		if err != nil {
			log.Printf("%+v\n", err)
			return
		}

		playEchoVideoSourceUrl, err := getPlayEchoVideoSource(playEchoVideoSourceeID)
		if err != nil {
			log.Printf("%+v\n", err)
			return
		}

		playEchoVideoSourceUrl = strings.Replace(playEchoVideoSourceUrl, "hlsx3cdn.burntburst45.store", "hlsx4cdn.burntburst45.store", 1)

		name := "storage/Download/" + fmt.Sprintf("%s/%s/%s - %d", request.Name, request.Season, request.Name, i) + ".%(ext)s"

		cmd := exec.Command("./yt-dlp_linux", "-o", name, "--referer", "https://play.echovideo.ru/", "--add-header", "Origin: https://play.echovideo.ru", playEchoVideoSourceUrl)

		// Redirect binary I/O directly to the terminal
		cmd.Stdout = os.Stdout
		cmd.Stderr = os.Stderr
		cmd.Stdin = os.Stdin

		// Run blocks until the command completes
		if err := cmd.Run(); err != nil {
			log.Printf("%+v\n", err)
			return
		}
	}
}

func extractSeasonID(u string) (string, error) {
	parsed, err := url.Parse(u)
	if err != nil {
		return "", nil
	}

	path := parsed.Path

	resource := strings.Split(path, "/")

	seasonID := resource[2][strings.LastIndex(resource[2], "-")+1:]

	return seasonID, nil
}

type GetEpisodeListResponse struct {
	Status int    `json:"status"`
	Result string `json:"result"`
}

func getEpisodeList(seasonID string) (int, error) {
	client := &http.Client{Timeout: 10 * time.Second}
	req, err := http.NewRequest("GET", fmt.Sprintf("https://aniwaves.ru/ajax/episode/list/%s?vrf=", seasonID), nil)
	if err != nil {
		return 0, err
	}

	req.Header.Add("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0")

	response, err := client.Do(req)
	if err != nil {
		return 0, err
	}

	defer response.Body.Close() // Essential: Close the body
	body, err := io.ReadAll(response.Body)
	if err != nil {
		return 0, err
	}

	lsr := GetEpisodeListResponse{}
	err = json.Unmarshal(body, &lsr)
	if err != nil {
		return 0, err
	}

	clean := html.UnescapeString(lsr.Result)

	wrapped := "<html><body>" + clean + "</body></html>"

	doc, err := goquery.NewDocumentFromReader(strings.NewReader(wrapped))
	if err != nil {
		return 0, err
	}

	count := doc.Find("ul[class='ep-range'] li").Length()

	return count, nil
}

type ListServersResponse struct {
	Status int    `json:"status"`
	Result string `json:"result"`
}

func getAniwaveSourceID(seasonID string, episode int) (string, error) {
	client := &http.Client{Timeout: 10 * time.Second}
	req, err := http.NewRequest("GET", fmt.Sprintf("https://aniwaves.ru/ajax/server/list?servers=%s&eps=%d", seasonID, episode), nil)
	if err != nil {
		return "", err
	}

	req.Header.Add("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0")

	response, err := client.Do(req)
	if err != nil {
		return "", err
	}

	defer response.Body.Close() // Essential: Close the body
	body, err := io.ReadAll(response.Body)
	if err != nil {
		return "", err
	}

	lsr := ListServersResponse{}
	err = json.Unmarshal(body, &lsr)
	if err != nil {
		return "", err
	}

	clean := html.UnescapeString(lsr.Result)
	wrapped := "<html><body>" + clean + "</body></html>"

	doc, err := goquery.NewDocumentFromReader(strings.NewReader(wrapped))
	if err != nil {
		return "", err
	}

	id, exist := doc.Find("div.type[data-type='dub'] ul li[data-sv-id='4']").Attr("data-link-id")
	if !exist {
		return "", errors.New("failed to find")
	}

	return id, nil
}

type SourceResponse struct {
	Status int `json:"status"`
	Result struct {
		Url      string `json:"url"`
		Server   int    `json:"server"`
		SkipData struct {
			Intro []int `json:"intro"`
			Outro []int `json:"outro"`
		} `json:"skip_data"`
		Sources   []interface{} `json:"sources"`
		Tracks    []interface{} `json:"tracks"`
		HtmlGuide string        `json:"htmlGuide"`
	} `json:"result"`
}

func getAniwavesSource(originalUrl, sourceID string) (string, error) {
	u := fmt.Sprintf("https://aniwaves.ru/ajax/sources?id=%s&asi=0&autoPlay=0", sourceID)

	client := &http.Client{Timeout: 10 * time.Second}
	req, err := http.NewRequest("GET", u, nil)
	if err != nil {
		return "", err
	}

	//GET /ajax/sources?id=UWxwb05ERkJXU1pUV1ZYT1k1b0FBQXQvLzl4TndCYk85aUtBc2tJeUpJd3d5Yzd3QzBCQzBnaWdVRWdIM2tzb0FnYUFvQUIwT0FBQUFHZ0FBQUFCazB4QWVtVFU5STlKNm1neWVvMEdtUU5BMDBCb0JvTkdnd2pSb1pQS0F5QTBCaHdCQU1NeUFja2pOTkxNcHRicTdmMkp4NFl4UDNFQXRKeUcxc3E0clpNTUl2bmhBdlphVzNJRDA5QTIvT2pqMTdBSmUySzkxVTBrUkVXYURKSmJjb1lXUWtYWUEzQXA0V1VRWFNHSWhoSno4ZnMzb3BTc1o3dnhFMjM2eit1ZnhkeVJUaFFrRlhPWTVvQT0=&asi=0&autoPlay=0 HTTP/2
	//Host: aniwaves.ru
	//User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0
	//Accept: application/json, text/javascript, */*; q=0.01
	//Accept-Language: en-US,en;q=0.9
	//Accept-Encoding: gzip, deflate, br, zstd
	//Referer: https://aniwaves.ru/watch/re-zero-kara-hajimeru-isekai-seikatsu-4th-season-82570/ep-1
	//X-Requested-With: XMLHttpRequest
	//Sec-Fetch-Dest: empty
	//Sec-Fetch-Mode: cors
	//Sec-Fetch-Site: same-origin
	//Connection: keep-alive
	//Cookie: cf_clearance=E7EaYC.6t5eW.AiGWFA4dGhrVUHTO7X8IiaRkfJTAco-1780105219-1.2.1.1-MJ0WuQfXydhynpkJCtY1Q719ENTQz4VLOfYm3pIdw7pfD41mgVQaml14Ca3gIw7pb.YmpANb8l5LPr9Boav4XmsH0pr0Ip58lVK5hkowCFzvMSFqJDdu_a43B9Km.FA5Ql3uPbXZeNU3rSlJuJ1wBDvKhhVFo4yIaxaWoIcfsqwNdmwuz81b_yWoIge0K0ciDAVKcwV.8uv9XfHP1T4Rrs_OKdKFRZ6cp0TBYjiLOe4NwFbWBviDIxsR8tDEhxKTof0UEVq9R0mBosRPelxr3YxIxH5QyFrCKdpghW.kDOeLXGfP13cYVkwZFwT0jXVhkLZvCkHEgpEZ9stzMERj0Y2P2xUx9WMKJRQaRyPMJWKUCn9FtZSvHrvuW23TaWOx9ll00D_ZPzmTHdtESQrc8MTgZ5wBDvPaXbyH1ksv2RA; prefered_server_id=4; prefered_server_type=dub; _ga_2BSQBMWMM9=GS2.1.s1780942892$o1$g1$t1780942910$j42$l0$h0; _ga=GA1.1.764185913.1780942893
	//Priority: u=0
	//Pragma: no-cache
	//Cache-Control: no-cache
	//TE: trailers

	req.Header.Add("Referer", originalUrl)
	req.Header.Add("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0")

	resp, err := client.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close() // Essential: Close the body
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}

	sr := SourceResponse{}
	err = json.Unmarshal(body, &sr)
	if err != nil {
		return "", err
	}

	return sr.Result.Url, nil
}

func extractSourceID(u string) (string, error) {
	parsedURL, err := url.Parse(u)
	if err != nil {
		return "", err
	}
	path := parsedURL.Path
	id := path[strings.LastIndex(path, "/")+1:]
	return id, nil
}

type PlayEchoVideoSourceResponse struct {
	Sources string `json:"sources"`
	Intro   struct {
		Start int `json:"start"`
		End   int `json:"end"`
	} `json:"intro"`
	Outro struct {
		Start int `json:"start"`
		End   int `json:"end"`
	} `json:"outro"`
}

func getPlayEchoVideoSource(id string) (string, error) {
	url := fmt.Sprintf("https://play.echovideo.ru/embed-1/getSources?id=%s", id)

	//GET /embed-1/getSources?id=KPpOQdEl-krSDv4eXEEESOGQxIyn39hxhDVL-a2XPou3CoyWNh4VQmK3H0rMoVhfkd0GovoHTaEZ6Sm5lyMSpt5yt-Dn2lZCmvH1yzTQSxaKaYl25DzAviI4tquisv4m4sdprfru0X7FRb516Rwgo1lBQipOxMdBWJAVmU3pVFbH-FCBQorpYyUMwrsEZeectZ9x8EhnJ_wYs7Sd2a1lnficzNBqKBC3zWOvZLXKnwY HTTP/2
	//Host: play.echovideo.ru
	//User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0
	//Accept: */*
	//Accept-Language: en-US,en;q=0.9
	//Accept-Encoding: gzip, deflate, br, zstd
	//Referer: https://play.echovideo.ru/embed-1/KPpOQdEl-krSDv4eXEEESOGQxIyn39hxhDVL-a2XPou3CoyWNh4VQmK3H0rMoVhfkd0GovoHTaEZ6Sm5lyMSpt5yt-Dn2lZCmvH1yzTQSxaKaYl25DzAviI4tquisv4m4sdprfru0X7FRb516Rwgo1lBQipOxMdBWJAVmU3pVFbH-FCBQorpYyUMwrsEZeectZ9x8EhnJ_wYs7Sd2a1lnficzNBqKBC3zWOvZLXKnwY?v=1&asi=0&autoPlay=0&ao=0&autostart=true
	//Connection: keep-alive
	//Sec-Fetch-Dest: empty
	//Sec-Fetch-Mode: cors
	//Sec-Fetch-Site: same-origin
	//Priority: u=4
	//Pragma: no-cache
	//Cache-Control: no-cache

	client := &http.Client{Timeout: 10 * time.Second}
	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return "", err
	}

	req.Header.Add("Referer", fmt.Sprintf("https://play.echovideo.ru/embed-1/%s?v=1&asi=0&autoPlay=0&ao=0&autostart=true", id))

	resp, err := client.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close() // Essential: Close the body
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}

	sr := PlayEchoVideoSourceResponse{}
	err = json.Unmarshal(body, &sr)
	if err != nil {
		return "", err
	}

	return sr.Sources, nil
}