all repos — fixyoutube-go @ 885bf1a0eb716d6fe63a385dc1bc5164564d4cf0

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

fixyoutube.go (view raw)

  1package main
  2
  3import (
  4	"bytes"
  5	"embed"
  6	"html/template"
  7	"io"
  8	"net/http"
  9	"net/url"
 10	"os"
 11	"regexp"
 12	"strconv"
 13	"time"
 14
 15	"github.com/BiRabittoh/fixyoutube-go/invidious"
 16	"github.com/joho/godotenv"
 17	"github.com/sirupsen/logrus"
 18)
 19
 20const templatesDirectory = "templates/"
 21
 22var (
 23	//go:embed templates/index.html templates/video.html
 24	templates      embed.FS
 25	apiKey         string
 26	logger         = logrus.New()
 27	indexTemplate  = template.Must(template.ParseFS(templates, templatesDirectory+"index.html"))
 28	videoTemplate  = template.Must(template.ParseFS(templates, templatesDirectory+"video.html"))
 29	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`)
 30	videoRegex     = regexp.MustCompile(`(?i)^[a-z0-9_-]{11}$`)
 31)
 32
 33func indexHandler(w http.ResponseWriter, r *http.Request) {
 34	buf := &bytes.Buffer{}
 35	err := indexTemplate.Execute(buf, nil)
 36	if err != nil {
 37		logger.Error("Failed to fill index template.")
 38		http.Error(w, err.Error(), http.StatusInternalServerError)
 39		return
 40	}
 41
 42	buf.WriteTo(w)
 43}
 44
 45func clearHandler(w http.ResponseWriter, r *http.Request) {
 46	if r.Method != http.MethodPost {
 47		http.Redirect(w, r, "/", http.StatusMovedPermanently)
 48		return
 49	}
 50
 51	err := r.ParseForm()
 52	if err != nil {
 53		logger.Error("Failed to parse form in /clear.")
 54		http.Error(w, err.Error(), http.StatusInternalServerError)
 55		return
 56	}
 57
 58	providedKey := r.PostForm.Get("apiKey")
 59	if providedKey != apiKey {
 60		logger.Debug("Wrong API key: ", providedKey)
 61		http.Error(w, "Wrong or missing API key.", http.StatusForbidden)
 62		return
 63	}
 64
 65	invidious.ClearDB()
 66	logger.Info("Cache cleared.")
 67	http.Error(w, "Done.", http.StatusOK)
 68}
 69
 70func videoHandler(videoId string, invidiousClient *invidious.Client, w http.ResponseWriter, r *http.Request) {
 71	url := "https://www.youtube.com/watch?v=" + videoId
 72	userAgent := r.UserAgent()
 73	res := userAgentRegex.MatchString(userAgent)
 74	if !res {
 75		logger.Debug("Regex did not match. Redirecting. UA:", userAgent)
 76		http.Redirect(w, r, url, http.StatusMovedPermanently)
 77		return
 78	}
 79
 80	if !videoRegex.MatchString(videoId) {
 81		logger.Info("Invalid video ID: ", videoId)
 82		http.Error(w, "Invalid video ID.", http.StatusBadRequest)
 83		return
 84	}
 85
 86	video, err := invidiousClient.GetVideo(videoId, true)
 87	if err != nil {
 88		logger.Info("Wrong video ID: ", videoId)
 89		http.Error(w, "Wrong video ID.", http.StatusNotFound)
 90		return
 91	}
 92
 93	if video.Url == "" {
 94		logger.Debug("No URL available. Redirecting.")
 95		http.Redirect(w, r, url, http.StatusMovedPermanently)
 96		return
 97	}
 98
 99	buf := &bytes.Buffer{}
100	err = videoTemplate.Execute(buf, video)
101	if err != nil {
102		logger.Error("Failed to fill video template.")
103		http.Error(w, err.Error(), http.StatusInternalServerError)
104		return
105	}
106	buf.WriteTo(w)
107}
108
109func watchHandler(invidiousClient *invidious.Client) http.HandlerFunc {
110	return func(w http.ResponseWriter, r *http.Request) {
111		u, err := url.Parse(r.URL.String())
112		if err != nil {
113			logger.Error("Failed to parse URL: ", r.URL.String())
114			http.Error(w, err.Error(), http.StatusInternalServerError)
115			return
116		}
117		q := u.Query()
118		videoId := q.Get("v")
119		videoHandler(videoId, invidiousClient, w, r)
120	}
121}
122
123func shortHandler(invidiousClient *invidious.Client) http.HandlerFunc {
124	return func(w http.ResponseWriter, r *http.Request) {
125		videoId := r.PathValue("videoId")
126		videoHandler(videoId, invidiousClient, w, r)
127	}
128}
129
130func proxyHandler(invidiousClient *invidious.Client) http.HandlerFunc {
131	return func(w http.ResponseWriter, r *http.Request) {
132		videoId := r.PathValue("videoId")
133
134		vb, s := invidiousClient.ProxyVideoId(videoId)
135		if s != http.StatusOK {
136			logger.Error("proxyHandler() failed. Final code: ", s)
137			http.Error(w, "Something went wrong.", s)
138			return
139		}
140		if !vb.ValidateLength() {
141			logger.Error("Buffer length is inconsistent.")
142			http.Error(w, "Something went wrong.", http.StatusInternalServerError)
143			return
144		}
145		h := w.Header()
146		h.Set("Status", "200")
147		h.Set("Content-Type", "video/mp4")
148		h.Set("Content-Length", strconv.FormatInt(vb.Length, 10))
149		io.Copy(w, vb.Buffer)
150	}
151}
152
153func getenvDefault(key string, def string) string {
154	res := os.Getenv(key)
155	if res == "" {
156		return def
157	}
158	return res
159}
160
161func getenvDefaultParse(key string, def string) float64 {
162	value := getenvDefault(key, def)
163	res, err := strconv.ParseFloat(value, 64)
164	if err != nil {
165		logger.Fatal(err)
166	}
167	return res
168}
169
170func main() {
171	logger.SetLevel(logrus.DebugLevel)
172	err := godotenv.Load()
173	if err != nil {
174		logger.Info("No .env file provided.")
175	}
176
177	apiKey = getenvDefault("API_KEY", "itsme")
178	port := getenvDefault("PORT", "3000")
179	cacheDuration := getenvDefaultParse("CACHE_DURATION_MINUTES", "5")
180	timeoutDuration := getenvDefaultParse("TIMEOUT_DURATION_MINUTES", "10")
181	cleanupInterval := getenvDefaultParse("CLEANUP_INTERVAL_SECONDS", "30")
182	maxSizeMB := getenvDefaultParse("MAX_SIZE_MB", "20")
183
184	myClient := &http.Client{Timeout: 10 * time.Second}
185	options := invidious.ClientOptions{
186		CacheDuration:   time.Duration(cacheDuration) * time.Minute,
187		TimeoutDuration: time.Duration(timeoutDuration) * time.Minute,
188		CleanupInterval: time.Duration(cleanupInterval) * time.Second,
189		MaxSizeBytes:    int64(maxSizeMB * 1000000),
190	}
191	videoapi := invidious.NewClient(myClient, options)
192
193	r := http.NewServeMux()
194	r.HandleFunc("/", indexHandler)
195	r.HandleFunc("/clear", clearHandler)
196	r.HandleFunc("/watch", watchHandler(videoapi))
197	r.HandleFunc("/proxy/{videoId}", proxyHandler(videoapi))
198	r.HandleFunc("/{videoId}", shortHandler(videoapi))
199	/*
200		// native go implementation
201		r := http.NewServeMux()
202		r.HandleFunc("/watch", watchHandler(videoapi))
203		r.HandleFunc("/{videoId}/", shortHandler(videoapi))
204		r.HandleFunc("/", indexHandler)
205	*/
206	logger.Info("Serving on port ", port)
207	http.ListenAndServe(":"+port, r)
208}