all repos — fixyoutube-go @ ca543233e814d5ac68644a39d8de37ee43f2119f

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