Newer
Older
anime-fetcher / main.go
frozendragon 4 hours ago 10 KB version 1
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
}