add download endpoint
Marco Andronaco andronacomarco@gmail.com
Sun, 24 Nov 2024 15:55:33 +0100
6 files changed,
110 insertions(+),
15 deletions(-)
M
fixyoutube.go
→
fixyoutube.go
@@ -60,10 +60,11 @@ burstTokens := getenvDefaultParse("BURST_TOKENS", "3")
rateLimit := getenvDefaultParse("RATE_LIMIT", "1") r := http.NewServeMux() - r.HandleFunc("/", indexHandler) - r.HandleFunc("/watch", watchHandler) - r.HandleFunc("/proxy/{videoId}", proxyHandler) - r.HandleFunc("/{videoId}", shortHandler) + r.HandleFunc("GET /", indexHandler) + r.HandleFunc("GET /watch", watchHandler) + r.HandleFunc("GET /proxy/{videoId}", proxyHandler) + r.HandleFunc("GET /{videoId}", shortHandler) + r.HandleFunc("POST /download", downloadHandler) var serveMux http.Handler if debugSwitch {
M
go.sum
→
go.sum
@@ -2,6 +2,8 @@ github.com/birabittoh/myks v0.0.2 h1:EBukMUsAflwiqdNo4LE7o2WQdEvawty5ewCZWY+IXSU=
github.com/birabittoh/myks v0.0.2/go.mod h1:klNWaeUWm7TmhnBHBMt9vALwCHW11/Xw1BpCNkCx7hs= github.com/birabittoh/rabbitpipe v0.0.2 h1:4ptBS4Ai9NJH9gv3uG5TZBp1H5gfgEabEw6XldSjUx0= github.com/birabittoh/rabbitpipe v0.0.2/go.mod h1:6cEDb0WpwrRm2vt5IO3s2gPjzhZZLP7gYx+l9e3gx1k= +github.com/birabittoh/rabbitpipe v0.0.5 h1:mHdAyeNKSDvZqnPZfUnrmKvD7JTzt1ztSrW5k4/opGM= +github.com/birabittoh/rabbitpipe v0.0.5/go.mod h1:6cEDb0WpwrRm2vt5IO3s2gPjzhZZLP7gYx+l9e3gx1k= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
M
handlers.go
→
handlers.go
@@ -1,16 +1,19 @@
package main import ( - "bytes" "embed" + "fmt" "io" + "log" "net/http" "net/url" "regexp" "strconv" + "strings" "text/template" "github.com/birabittoh/fixyoutube-go/invidious" + "github.com/birabittoh/rabbitpipe" ) const templatesDirectory = "templates/"@@ -19,21 +22,55 @@ var (
//go:embed templates/index.html templates/video.html templates embed.FS indexTemplate = template.Must(template.ParseFS(templates, templatesDirectory+"index.html")) - videoTemplate = template.Must(template.ParseFS(templates, templatesDirectory+"video.html")) + videoTemplate = template.Must(template.New("video.html").Funcs(template.FuncMap{"parseFormat": parseFormat}).ParseFS(templates, templatesDirectory+"video.html")) // userAgentRegex = regexp.MustCompile(`(?i)bot|facebook|embed|got|firefox\/92|firefox\/38|curl|wget|go-http|yahoo|generator|whatsapp|preview|link|proxy|vkshare|images|analyzer|index|crawl|spider|python|cfnetwork|node`) videoRegex = regexp.MustCompile(`(?i)^[a-z0-9_-]{11}$`) ) +func parseFormat(f rabbitpipe.Format) (res string) { + isAudio := f.AudioChannels > 0 + + if isAudio { + bitrate, err := strconv.Atoi(f.Bitrate) + if err != nil { + logger.Error("Failed to convert bitrate to integer.") + return + } + res = strconv.Itoa(bitrate/1000) + "kbps" + } else { + res = f.Resolution + } + + mime := strings.Split(f.Type, ";") + res += " - " + mime[0] + + codecs := " (" + strings.Split(mime[1], "\"")[1] + ")" + + if !isAudio { + res += fmt.Sprintf(" (%d FPS)", f.FPS) + } + + res += codecs + return +} + +func getItag(formats []rabbitpipe.Format, itag string) *rabbitpipe.Format { + for _, f := range formats { + if f.Itag == itag { + return &f + } + } + + return nil +} + func indexHandler(w http.ResponseWriter, r *http.Request) { - buf := &bytes.Buffer{} - err := indexTemplate.Execute(buf, nil) + err := indexTemplate.Execute(w, nil) if err != nil { logger.Error("Failed to fill index template.") http.Error(w, err.Error(), http.StatusInternalServerError) return } - - buf.WriteTo(w) } func videoHandler(videoID string, w http.ResponseWriter, r *http.Request) {@@ -58,14 +95,12 @@ http.Redirect(w, r, url, http.StatusFound)
return } - buf := &bytes.Buffer{} - err = videoTemplate.Execute(buf, video) + err = videoTemplate.Execute(w, video) if err != nil { logger.Error("Failed to fill video template.") http.Error(w, err.Error(), http.StatusInternalServerError) return } - buf.WriteTo(w) } func watchHandler(w http.ResponseWriter, r *http.Request) {@@ -107,3 +142,40 @@ h.Set("Content-Type", "video/mp4")
h.Set("Content-Length", strconv.FormatInt(vb.Length, 10)) io.Copy(w, vb.Buffer) } + +func downloadHandler(w http.ResponseWriter, r *http.Request) { + videoID := r.FormValue("video") + if videoID == "" { + http.Error(w, "Missing video ID", http.StatusBadRequest) + return + } + + if !videoRegex.MatchString(videoID) { + log.Println("Invalid video ID:", videoID) + http.Error(w, "not found", http.StatusNotFound) + return + } + + itag := r.FormValue("itag") + if itag == "" { + http.Error(w, "not found", http.StatusBadRequest) + return + } + + video, err := invidious.RP.GetVideo(videoID) + if err != nil || video == nil { + http.Error(w, "not found", http.StatusNotFound) + return + } + + format := getItag(video.FormatStreams, itag) + if format == nil { + format = getItag(video.AdaptiveFormats, itag) + if format == nil { + http.Error(w, "not found", http.StatusNotFound) + return + } + } + + http.Redirect(w, r, format.URL, http.StatusFound) +}
M
invidious/invidious.go
→
invidious/invidious.go
@@ -11,7 +11,7 @@ )
var logger = logrus.New() var buffers = myks.New[VideoBuffer](time.Minute) -var RP = rabbitpipe.New() +var RP = rabbitpipe.New("") type VideoBuffer struct { Buffer *bytes.Buffer
M
templates/video.html
→
templates/video.html
@@ -47,6 +47,26 @@ {{ end }}
<h2>{{ .Title }}</h2> <h3>> <a href="https://www.youtube.com/{{ .AuthorURL }}" target="_blank">{{ .Author }}</a></h3> <pre style="white-space: pre-wrap">{{ .DescriptionHTML }}</pre> + <form action="/download" method="post" rel="noopener" target="_blank" style="display: grid; grid-template-columns: auto auto; justify-content: space-between;"> + <input type="hidden" name="video" value="{{ .VideoID }}"> + <select name="itag"> + <optgroup label="Full"> + {{ range .FormatStreams }} + <option value="{{ .Itag }}"> + {{ parseFormat . }} + </option> + {{ end }} + </optgroup> + <optgroup label="Partial"> + {{ range .AdaptiveFormats }} + <option value="{{ .Itag }}"> + {{ parseFormat . }} + </option> + {{ end }} + </optgroup> + </select> + <button type="submit">Download</button> + </form> <a href="https://www.youtube.com/watch?v={{ .VideoID }}">Watch on YouTube</a> <br /> <a href="/">What is this?</a>