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"
"html"
"io"
"log"
"net/http"
"net/url"
"os"
"os/exec"
"strings"
"time"
)
func main() {
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.Post("/session", postSession)
http.ListenAndServe(":3000", r)
}
func postSession(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")
seasonID, err := extractSeasonID(u)
if err != nil {
log.Printf("%+v\n", err)
http.Redirect(w, r, "/failed", http.StatusTemporaryRedirect)
return
}
episodeCount, err := getEpisodeList(seasonID)
if err != nil {
log.Printf("%+v\n", err)
http.Redirect(w, r, "/failed", http.StatusTemporaryRedirect)
return
}
for i := 1; i < episodeCount; i++ {
sourceID, err := getAniwaveSourceID(seasonID, i)
if err != nil {
log.Printf("%+v\n", err)
http.Redirect(w, r, "/failed", http.StatusTemporaryRedirect)
return
}
sourceUrl, err := getAniwavesSource(sourceID)
if err != nil {
log.Printf("%+v\n", err)
http.Redirect(w, r, "/failed", http.StatusTemporaryRedirect)
return
}
playEchoVideoSourceeID, err := extractSourceID(sourceUrl)
if err != nil {
log.Printf("%+v\n", err)
http.Redirect(w, r, "/failed", http.StatusTemporaryRedirect)
return
}
playEchoVideoSourceUrl, err := getPlayEchoVideoSource(playEchoVideoSourceeID)
if err != nil {
log.Printf("%+v\n", err)
http.Redirect(w, r, "/failed", http.StatusTemporaryRedirect)
return
}
name := "storage/Download/" + fmt.Sprintf("%s/%s/%s - %d", name, season, 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)
http.Redirect(w, r, "/failed", http.StatusTemporaryRedirect)
return
}
http.Redirect(w, r, "/success", http.StatusTemporaryRedirect)
}
}
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(sourceID string) (string, error) {
url := 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", url, 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", "https://aniwaves.ru/watch/re-zero-kara-hajimeru-isekai-seikatsu-4th-season-82570/ep-1")
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
}