all repos — fixyoutube-go @ f1072314494f5d5e72b46d17768a5a3509c01f6c

A better way to embed YouTube videos everywhere (inspired by FixTweet).

handlers.go (view raw)

  1package main
  2
  3import (
  4	"embed"
  5	"fmt"
  6	"io"
  7	"log"
  8	"net/http"
  9	"net/url"
 10	"regexp"
 11	"strconv"
 12	"strings"
 13	"text/template"
 14
 15	"github.com/birabittoh/fixyoutube-go/invidious"
 16	"github.com/birabittoh/rabbitpipe"
 17)
 18
 19const templatesDirectory = "templates/"
 20
 21var (
 22	//go:embed templates/index.html templates/video.html
 23	templates     embed.FS
 24	indexTemplate = template.Must(template.ParseFS(templates, templatesDirectory+"index.html"))
 25	videoTemplate = template.Must(template.New("video.html").Funcs(template.FuncMap{"parseFormat": parseFormat}).ParseFS(templates, templatesDirectory+"video.html"))
 26	// 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`)
 27	videoRegex = regexp.MustCompile(`(?i)^[a-z0-9_-]{11}$`)
 28)
 29
 30func parseFormat(f rabbitpipe.Format) (res string) {
 31	isAudio := f.AudioChannels > 0
 32
 33	if isAudio {
 34		bitrate, err := strconv.Atoi(f.Bitrate)
 35		if err != nil {
 36			logger.Error("Failed to convert bitrate to integer.")
 37			return
 38		}
 39		res = strconv.Itoa(bitrate/1000) + "kbps"
 40	} else {
 41		res = f.Resolution
 42	}
 43
 44	mime := strings.Split(f.Type, ";")
 45	res += " - " + mime[0]
 46
 47	codecs := " (" + strings.Split(mime[1], "\"")[1] + ")"
 48
 49	if !isAudio {
 50		res += fmt.Sprintf(" (%d FPS)", f.FPS)
 51	}
 52
 53	res += codecs
 54	return
 55}
 56
 57func getItag(formats []rabbitpipe.Format, itag string) *rabbitpipe.Format {
 58	for _, f := range formats {
 59		if f.Itag == itag {
 60			return &f
 61		}
 62	}
 63
 64	return nil
 65}
 66
 67func indexHandler(w http.ResponseWriter, r *http.Request) {
 68	err := indexTemplate.Execute(w, nil)
 69	if err != nil {
 70		logger.Error("Failed to fill index template.")
 71		http.Error(w, err.Error(), http.StatusInternalServerError)
 72		return
 73	}
 74}
 75
 76func videoHandler(videoID string, w http.ResponseWriter, r *http.Request) {
 77	url := "https://www.youtube.com/watch?v=" + videoID
 78
 79	if !videoRegex.MatchString(videoID) {
 80		logger.Info("Invalid video ID: ", videoID)
 81		http.Error(w, "Invalid video ID.", http.StatusBadRequest)
 82		return
 83	}
 84
 85	video, err := invidious.RP.GetVideo(videoID)
 86	if err != nil || video == nil {
 87		logger.Info("Wrong video ID: ", videoID)
 88		http.Error(w, "Wrong video ID.", http.StatusNotFound)
 89		return
 90	}
 91
 92	if invidious.GetVideoURL(*video) == "" {
 93		logger.Debug("No URL available. Redirecting.")
 94		http.Redirect(w, r, url, http.StatusFound)
 95		return
 96	}
 97
 98	err = videoTemplate.Execute(w, video)
 99	if err != nil {
100		logger.Error("Failed to fill video template.")
101		http.Error(w, err.Error(), http.StatusInternalServerError)
102		return
103	}
104}
105
106func watchHandler(w http.ResponseWriter, r *http.Request) {
107	u, err := url.Parse(r.URL.String())
108	if err != nil {
109		logger.Error("Failed to parse URL: ", r.URL.String())
110		http.Error(w, err.Error(), http.StatusInternalServerError)
111		return
112	}
113
114	q := u.Query()
115	videoID := q.Get("v")
116	videoHandler(videoID, w, r)
117}
118
119func shortHandler(w http.ResponseWriter, r *http.Request) {
120	videoID := r.PathValue("videoID")
121	videoHandler(videoID, w, r)
122}
123
124func proxyHandler(w http.ResponseWriter, r *http.Request) {
125	videoID := r.PathValue("videoID")
126
127	vb, s := invidious.ProxyVideoId(videoID)
128	if s != http.StatusOK {
129		logger.Error("proxyHandler() failed. Final code: ", s)
130		http.Error(w, http.StatusText(s), s)
131		return
132	}
133	if !vb.ValidateLength() {
134		logger.Error("Buffer length is inconsistent.")
135		status := http.StatusInternalServerError
136		http.Error(w, http.StatusText(status), status)
137		return
138	}
139	h := w.Header()
140	h.Set("Status", "200")
141	h.Set("Content-Type", "video/mp4")
142	h.Set("Content-Length", strconv.FormatInt(vb.Length, 10))
143	io.Copy(w, vb.Buffer)
144}
145
146func downloadHandler(w http.ResponseWriter, r *http.Request) {
147	videoID := r.FormValue("video")
148	if videoID == "" {
149		http.Error(w, "Missing video ID", http.StatusBadRequest)
150		return
151	}
152
153	if !videoRegex.MatchString(videoID) {
154		log.Println("Invalid video ID:", videoID)
155		http.Error(w, "not found", http.StatusNotFound)
156		return
157	}
158
159	itag := r.FormValue("itag")
160	if itag == "" {
161		http.Error(w, "not found", http.StatusBadRequest)
162		return
163	}
164
165	video, err := invidious.RP.GetVideo(videoID)
166	if err != nil || video == nil {
167		http.Error(w, "not found", http.StatusNotFound)
168		return
169	}
170
171	format := getItag(video.FormatStreams, itag)
172	if format == nil {
173		format = getItag(video.AdaptiveFormats, itag)
174		if format == nil {
175			http.Error(w, "not found", http.StatusNotFound)
176			return
177		}
178	}
179
180	http.Redirect(w, r, format.URL, http.StatusFound)
181}
182
183func refreshHandler(w http.ResponseWriter, r *http.Request) {
184	videoID := r.PathValue("videoID")
185	if videoID == "" {
186		http.Error(w, "bad request", http.StatusBadRequest)
187		return
188	}
189
190	if !videoRegex.MatchString(videoID) {
191		http.Error(w, "not found", http.StatusNotFound)
192		return
193	}
194
195	video, err := invidious.RP.GetVideoNoCache(videoID)
196	if err != nil || video == nil {
197		http.Error(w, "not found", http.StatusNotFound)
198		return
199	}
200
201	http.Redirect(w, r, "/"+videoID, http.StatusFound)
202}