all repos — fixyoutube-go @ c9e6e46c2ff2b9496e106388db2f66bef0879330

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

fixyoutube.go (view raw)

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