all repos — fixyoutube-go @ a07abe81886576fd49175b5d58b10eba22d53029

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 templates/cache.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	cacheTemplate = template.Must(template.New("cache.html").ParseFS(templates, templatesDirectory+"cache.html"))
 27
 28	// 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`)
 29	videoRegex = regexp.MustCompile(`(?i)^[a-z0-9_-]{11}$`)
 30
 31	adminUser string
 32	adminPass string
 33)
 34
 35func defaultError(w http.ResponseWriter, code int) {
 36	http.Error(w, http.StatusText(code), code)
 37}
 38
 39func parseFormat(f rabbitpipe.Format) (res string) {
 40	isAudio := f.AudioChannels > 0
 41
 42	if isAudio {
 43		bitrate, err := strconv.Atoi(f.Bitrate)
 44		if err != nil {
 45			logger.Error("Failed to convert bitrate to integer.")
 46			return
 47		}
 48		res = strconv.Itoa(bitrate/1000) + "kbps"
 49	} else {
 50		res = f.Resolution
 51	}
 52
 53	mime := strings.Split(f.Type, ";")
 54	res += " - " + mime[0]
 55
 56	codecs := " (" + strings.Split(mime[1], "\"")[1] + ")"
 57
 58	if !isAudio {
 59		res += fmt.Sprintf(" (%d FPS)", f.FPS)
 60	}
 61
 62	res += codecs
 63	return
 64}
 65
 66func getItag(formats []rabbitpipe.Format, itag string) *rabbitpipe.Format {
 67	for _, f := range formats {
 68		if f.Itag == itag {
 69			return &f
 70		}
 71	}
 72
 73	return nil
 74}
 75
 76func indexHandler(w http.ResponseWriter, r *http.Request) {
 77	err := indexTemplate.Execute(w, nil)
 78	if err != nil {
 79		logger.Error("Failed to fill index template.")
 80		defaultError(w, http.StatusInternalServerError)
 81		return
 82	}
 83}
 84
 85func videoHandler(videoID string, w http.ResponseWriter, r *http.Request) {
 86	url := "https://www.youtube.com/watch?v=" + videoID
 87
 88	if !videoRegex.MatchString(videoID) {
 89		logger.Info("Invalid video ID: ", videoID)
 90		http.Error(w, "Invalid video ID.", http.StatusBadRequest)
 91		return
 92	}
 93
 94	video, err := invidious.RP.GetVideo(videoID)
 95	if err != nil || video == nil {
 96		logger.Info("Wrong video ID: ", videoID)
 97		http.Error(w, "Wrong video ID.", http.StatusNotFound)
 98		return
 99	}
100
101	if invidious.GetVideoURL(*video) == "" {
102		logger.Debug("No URL available. Redirecting.")
103		http.Redirect(w, r, url, http.StatusFound)
104		return
105	}
106
107	err = videoTemplate.Execute(w, video)
108	if err != nil {
109		logger.Error("Failed to fill video template.")
110		http.Error(w, err.Error(), http.StatusInternalServerError)
111		return
112	}
113}
114
115func watchHandler(w http.ResponseWriter, r *http.Request) {
116	u, err := url.Parse(r.URL.String())
117	if err != nil {
118		logger.Error("Failed to parse URL: ", r.URL.String())
119		http.Error(w, err.Error(), http.StatusInternalServerError)
120		return
121	}
122
123	q := u.Query()
124	videoID := q.Get("v")
125	videoHandler(videoID, w, r)
126}
127
128func shortHandler(w http.ResponseWriter, r *http.Request) {
129	videoID := r.PathValue("videoID")
130	videoHandler(videoID, w, r)
131}
132
133func proxyHandler(w http.ResponseWriter, r *http.Request) {
134	videoID := r.PathValue("videoID")
135
136	vb, s := invidious.ProxyVideoId(videoID)
137	if s != http.StatusOK {
138		logger.Error("proxyHandler() failed. Final code: ", s)
139		http.Error(w, http.StatusText(s), s)
140		return
141	}
142	if !vb.ValidateLength() {
143		logger.Error("Buffer length is inconsistent.")
144		status := http.StatusInternalServerError
145		http.Error(w, http.StatusText(status), status)
146		return
147	}
148	h := w.Header()
149	h.Set("Status", "200")
150	h.Set("Content-Type", "video/mp4")
151	h.Set("Content-Length", strconv.FormatInt(vb.Length, 10))
152	io.Copy(w, vb.Buffer)
153}
154
155func subHandler(w http.ResponseWriter, r *http.Request) {
156	videoID := r.PathValue("videoID")
157	language := r.PathValue("language")
158
159	captions, err := invidious.RP.GetCaptions(videoID, language)
160	if err != nil {
161		logger.Error("Failed to get captions: ", err)
162		defaultError(w, http.StatusNotFound)
163		return
164	}
165
166	w.Header().Set("Content-Type", "text/vtt")
167	w.Header().Set("Content-Length", strconv.Itoa(len(captions)))
168
169	_, err = w.Write(captions)
170	if err != nil {
171		defaultError(w, http.StatusInternalServerError)
172		return
173	}
174}
175
176func downloadHandler(w http.ResponseWriter, r *http.Request) {
177	videoID := r.FormValue("video")
178	if videoID == "" {
179		http.Error(w, "Missing video ID", http.StatusBadRequest)
180		return
181	}
182
183	if !videoRegex.MatchString(videoID) {
184		logger.Println("Invalid video ID:", videoID)
185		http.Error(w, "not found", http.StatusNotFound)
186		return
187	}
188
189	itag := r.FormValue("itag")
190	if itag == "" {
191		http.Error(w, "not found", http.StatusBadRequest)
192		return
193	}
194
195	video, err := invidious.RP.GetVideo(videoID)
196	if err != nil || video == nil {
197		http.Error(w, "not found", http.StatusNotFound)
198		return
199	}
200
201	format := getItag(video.FormatStreams, itag)
202	if format == nil {
203		format = getItag(video.AdaptiveFormats, itag)
204		if format == nil {
205			http.Error(w, "not found", http.StatusNotFound)
206			return
207		}
208	}
209
210	http.Redirect(w, r, format.URL, http.StatusFound)
211}
212
213func refreshHandler(w http.ResponseWriter, r *http.Request) {
214	videoID := r.PathValue("videoID")
215	if videoID == "" {
216		http.Error(w, "bad request", http.StatusBadRequest)
217		return
218	}
219
220	if !videoRegex.MatchString(videoID) {
221		http.Error(w, "not found", http.StatusNotFound)
222		return
223	}
224
225	video, err := invidious.RP.GetVideoNoCache(videoID)
226	if err != nil || video == nil {
227		http.Error(w, "not found", http.StatusNotFound)
228		return
229	}
230
231	http.Redirect(w, r, "/"+videoID, http.StatusFound)
232}
233
234func cacheHandler(w http.ResponseWriter, r *http.Request) {
235	username, password, ok := r.BasicAuth()
236	if !ok || username != adminUser || password != adminPass {
237		w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
238		http.Error(w, "unauthorized", http.StatusUnauthorized)
239		return
240	}
241
242	var videos []rabbitpipe.Video
243	for s := range invidious.RP.GetCachedVideos() {
244		video, err := invidious.RP.GetVideo(s)
245		if err != nil || video == nil {
246			continue
247		}
248		videos = append(videos, *video)
249	}
250
251	err := cacheTemplate.Execute(w, videos)
252	if err != nil {
253		log.Println("cacheHandler ERROR:", err)
254		http.Error(w, "internal server error", http.StatusInternalServerError)
255		return
256	}
257}