all repos — fixyoutube-go @ f2f8edfc003d89ede8c1246a415921d151be7123

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 parseFormatIndex(formatIndexString string) int {
 31	formatIndex, err := strconv.Atoi(formatIndexString)
 32	if err != nil || formatIndex < 0 {
 33		logger.Debug("Could not parse formatIndex.")
 34		return 0
 35	}
 36	return formatIndex
 37}
 38
 39func indexHandler(w http.ResponseWriter, r *http.Request) {
 40	buf := &bytes.Buffer{}
 41	err := indexTemplate.Execute(buf, nil)
 42	if err != nil {
 43		logger.Error("Failed to fill index template.")
 44		http.Error(w, err.Error(), http.StatusInternalServerError)
 45		return
 46	}
 47
 48	buf.WriteTo(w)
 49}
 50
 51func clearHandler(w http.ResponseWriter, r *http.Request) {
 52	if r.Method != http.MethodPost {
 53		http.Redirect(w, r, "/", http.StatusMovedPermanently)
 54		return
 55	}
 56
 57	err := r.ParseForm()
 58	if err != nil {
 59		logger.Error("Failed to parse form in /clear.")
 60		http.Error(w, err.Error(), http.StatusInternalServerError)
 61		return
 62	}
 63
 64	providedKey := r.PostForm.Get("apiKey")
 65	if providedKey != apiKey {
 66		logger.Debug("Wrong API key: ", providedKey)
 67		http.Error(w, "Wrong or missing API key.", http.StatusForbidden)
 68		return
 69	}
 70
 71	invidious.ClearDB()
 72	logger.Info("Cache cleared.")
 73	http.Error(w, "Done.", http.StatusOK)
 74}
 75
 76func videoHandler(videoId string, formatIndex int, invidiousClient *invidious.Client, w http.ResponseWriter, r *http.Request) {
 77	userAgent := r.UserAgent()
 78	res := userAgentRegex.MatchString(userAgent)
 79	if !res {
 80		logger.Debug("Regex did not match. Redirecting. UA:", userAgent)
 81		url := "https://www.youtube.com/watch?v=" + videoId
 82		http.Redirect(w, r, url, http.StatusMovedPermanently)
 83		return
 84	}
 85
 86	if !videoRegex.MatchString(videoId) {
 87		logger.Info("Invalid video ID: ", videoId)
 88		http.Error(w, "Invalid video ID.", http.StatusBadRequest)
 89		return
 90	}
 91
 92	video, err := invidiousClient.GetVideo(videoId, true)
 93	if err != nil {
 94		logger.Info("Wrong video ID: ", videoId)
 95		http.Error(w, "Wrong video ID.", http.StatusNotFound)
 96		return
 97	}
 98
 99	video.FormatIndex = formatIndex % len(video.Formats)
100
101	buf := &bytes.Buffer{}
102	err = videoTemplate.Execute(buf, video)
103	if err != nil {
104		logger.Error("Failed to fill video template.")
105		http.Error(w, err.Error(), http.StatusInternalServerError)
106		return
107	}
108	buf.WriteTo(w)
109}
110
111func watchHandler(invidiousClient *invidious.Client) http.HandlerFunc {
112	return func(w http.ResponseWriter, r *http.Request) {
113		u, err := url.Parse(r.URL.String())
114		if err != nil {
115			logger.Error("Failed to parse URL: ", r.URL.String())
116			http.Error(w, err.Error(), http.StatusInternalServerError)
117			return
118		}
119		q := u.Query()
120		videoId := q.Get("v")
121		formatIndex := parseFormatIndex(q.Get("f"))
122		videoHandler(videoId, formatIndex, invidiousClient, w, r)
123	}
124}
125
126func shortHandler(invidiousClient *invidious.Client) http.HandlerFunc {
127	return func(w http.ResponseWriter, r *http.Request) {
128		vars := mux.Vars(r)
129		videoId := vars["videoId"]
130		formatIndex := parseFormatIndex(vars["formatIndex"])
131		videoHandler(videoId, formatIndex, invidiousClient, w, r)
132	}
133}
134
135func proxyHandler(invidiousClient *invidious.Client) http.HandlerFunc {
136	return func(w http.ResponseWriter, r *http.Request) {
137		vars := mux.Vars(r)
138		videoId := vars["videoId"]
139		formatIndex := parseFormatIndex(vars["formatIndex"])
140
141		video, err := invidious.GetVideoDB(videoId)
142		if err != nil {
143			logger.Warn("Cannot proxy a video that is not cached: https://youtu.be/", videoId)
144			http.Error(w, "Something went wrong.", http.StatusBadRequest)
145			return
146		}
147
148		fmtAmount := len(video.Formats)
149		idx := formatIndex % fmtAmount
150
151		var httpStatus = http.StatusNotFound
152
153		for i := fmtAmount - 1 - idx; i >= 0; i-- {
154			url := video.Formats[i].Url
155			b, l, httpStatus := invidiousClient.ProxyVideo(url, formatIndex)
156			switch httpStatus {
157			case http.StatusOK:
158				header := w.Header()
159				header.Set("Status", "200")
160				header.Set("Content-Type", "video/mp4")
161				header.Set("Content-Length", strconv.FormatInt(l, 10))
162				io.Copy(w, b)
163				return
164			case http.StatusBadRequest:
165				continue
166			default:
167				i = -1
168			}
169		}
170		logger.Error("proxyHandler() failed. Code: ", httpStatus)
171		http.Error(w, "Something went wrong.", httpStatus)
172	}
173}
174
175func main() {
176	logger.SetLevel(logrus.DebugLevel)
177	err := godotenv.Load()
178	if err != nil {
179		logger.Info("No .env file provided.")
180	}
181
182	port := os.Getenv("PORT")
183	if port == "" {
184		port = "3000"
185	}
186
187	apiKey = os.Getenv("API_KEY")
188	if apiKey == "" {
189		apiKey = "itsme"
190	}
191
192	myClient := &http.Client{Timeout: 10 * time.Second}
193	videoapi := invidious.NewClient(myClient)
194
195	r := mux.NewRouter()
196	r.HandleFunc("/", indexHandler)
197	r.HandleFunc("/clear", clearHandler)
198	r.HandleFunc("/watch", watchHandler(videoapi))
199	r.HandleFunc("/proxy/{videoId}", proxyHandler(videoapi))
200	r.HandleFunc("/proxy/{videoId}/{formatIndex}", proxyHandler(videoapi))
201	r.HandleFunc("/{videoId}", shortHandler(videoapi))
202	r.HandleFunc("/{videoId}/{formatIndex}", shortHandler(videoapi))
203	/*
204		// native go implementation (useless until february 2024)
205		r := http.NewServeMux()
206		r.HandleFunc("/watch", watchHandler(videoapi))
207		r.HandleFunc("/{videoId}/", shortHandler(videoapi))
208		r.HandleFunc("/", indexHandler)
209	*/
210	logger.Info("Serving on port ", port)
211	http.ListenAndServe(":"+port, r)
212}