all repos — fixyoutube-go @ 8af1063c2c53e5a2aed2ded07e582eddae6815fc

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