all repos — fixyoutube-go @ df3fa40015c39d0e0a70606293bfec8a7a31fa55

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