all repos — fixyoutube-go @ 913b5e94318ac196a2c6111e459ffe2ba801cd36

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	"net/http"
  7	"net/url"
  8	"os"
  9	"regexp"
 10	"strconv"
 11	"time"
 12
 13	"github.com/BiRabittoh/fixyoutube-go/invidious"
 14	"github.com/gorilla/mux"
 15	"github.com/joho/godotenv"
 16	"github.com/sirupsen/logrus"
 17)
 18
 19const templatesDirectory = "templates/"
 20
 21var logger = logrus.New()
 22var indexTemplate = template.Must(template.ParseFiles(templatesDirectory + "index.html"))
 23var videoTemplate = template.Must(template.ParseFiles(templatesDirectory + "video.html"))
 24var 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`)
 25var videoRegex = regexp.MustCompile(`^(?i)[a-z0-9_-]{11}$`)
 26
 27var apiKey string
 28
 29func parseFormatIndex(formatIndexString string) int {
 30	formatIndex, err := strconv.Atoi(formatIndexString)
 31	if err != nil || formatIndex < 0 {
 32		logger.Debug("Could not parse formatIndex.")
 33		return 0
 34	}
 35	return formatIndex
 36}
 37
 38func indexHandler(w http.ResponseWriter, r *http.Request) {
 39	buf := &bytes.Buffer{}
 40	err := indexTemplate.Execute(buf, nil)
 41	if err != nil {
 42		logger.Error("Failed to fill index template.")
 43		http.Error(w, err.Error(), http.StatusInternalServerError)
 44		return
 45	}
 46
 47	buf.WriteTo(w)
 48}
 49
 50func clearHandler(w http.ResponseWriter, r *http.Request) {
 51	if r.Method != http.MethodPost {
 52		http.Redirect(w, r, "/", http.StatusMovedPermanently)
 53		return
 54	}
 55
 56	err := r.ParseForm()
 57	if err != nil {
 58		logger.Error("Failed to parse form in /clear.")
 59		http.Error(w, err.Error(), http.StatusInternalServerError)
 60		return
 61	}
 62
 63	providedKey := r.PostForm.Get("apiKey")
 64	if providedKey != apiKey {
 65		logger.Debug("Wrong API key: ", providedKey)
 66		http.Error(w, "Wrong or missing API key.", http.StatusForbidden)
 67		return
 68	}
 69
 70	invidious.ClearDB()
 71	logger.Info("Cache cleared.")
 72	http.Error(w, "Done.", http.StatusOK)
 73}
 74
 75func videoHandler(videoId string, formatIndex int, invidiousClient *invidious.Client, w http.ResponseWriter, r *http.Request) {
 76	userAgent := r.UserAgent()
 77	res := userAgentRegex.MatchString(userAgent)
 78	if !res {
 79		logger.Debug("Regex did not match. Redirecting. UA:", userAgent)
 80		url := "https://www.youtube.com/watch?v=" + videoId
 81		http.Redirect(w, r, url, http.StatusMovedPermanently)
 82		return
 83	}
 84
 85	if !videoRegex.MatchString(videoId) {
 86		logger.Info("Invalid video ID: ", videoId)
 87		http.Error(w, "Invalid video ID.", http.StatusBadRequest)
 88		return
 89	}
 90
 91	video, err := invidiousClient.GetVideo(videoId)
 92	if err != nil {
 93		logger.Info("Wrong video ID: ", videoId)
 94		http.Error(w, "Wrong video ID.", http.StatusNotFound)
 95		return
 96	}
 97
 98	video.FormatIndex = formatIndex % len(video.Formats)
 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		formatIndex := parseFormatIndex(q.Get("f"))
121		videoHandler(videoId, formatIndex, invidiousClient, w, r)
122	}
123}
124
125func shortHandler(invidiousClient *invidious.Client) http.HandlerFunc {
126	return func(w http.ResponseWriter, r *http.Request) {
127		vars := mux.Vars(r)
128		videoId := vars["videoId"]
129		formatIndex := parseFormatIndex(vars["formatIndex"])
130		videoHandler(videoId, formatIndex, invidiousClient, w, r)
131	}
132}
133
134func proxyHandler(invidiousClient *invidious.Client) http.HandlerFunc {
135	return func(w http.ResponseWriter, r *http.Request) {
136		vars := mux.Vars(r)
137		videoId := vars["videoId"]
138		formatIndex := parseFormatIndex(vars["formatIndex"])
139		invidiousClient.ProxyVideo(w, videoId, formatIndex)
140	}
141}
142
143func main() {
144	err := godotenv.Load()
145	if err != nil {
146		logger.Info("No .env file provided.")
147	}
148
149	port := os.Getenv("PORT")
150	if port == "" {
151		port = "3000"
152	}
153
154	apiKey = os.Getenv("API_KEY")
155	if apiKey == "" {
156		apiKey = "itsme"
157	}
158
159	myClient := &http.Client{Timeout: 10 * time.Second}
160	videoapi := invidious.NewClient(myClient)
161
162	r := mux.NewRouter()
163	r.HandleFunc("/", indexHandler)
164	r.HandleFunc("/clear", clearHandler)
165	r.HandleFunc("/watch", watchHandler(videoapi))
166	r.HandleFunc("/proxy/{videoId}", proxyHandler(videoapi))
167	r.HandleFunc("/proxy/{videoId}/{formatIndex}", proxyHandler(videoapi))
168	r.HandleFunc("/{videoId}", shortHandler(videoapi))
169	r.HandleFunc("/{videoId}/{formatIndex}", shortHandler(videoapi))
170	/*
171		// native go implementation (useless until february 2024)
172		r := http.NewServeMux()
173		r.HandleFunc("/watch", watchHandler(videoapi))
174		r.HandleFunc("/{videoId}/", shortHandler(videoapi))
175		r.HandleFunc("/", indexHandler)
176	*/
177	logger.Info("Serving on port ", port)
178	http.ListenAndServe(":"+port, r)
179}