all repos — fixyoutube-go @ a463db2dc3d1b2e9bbe391a4b2db4a173d21b347

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	"log"
  7	"net/http"
  8	"net/url"
  9	"os"
 10	"regexp"
 11	"slices"
 12	"strconv"
 13	"time"
 14
 15	"github.com/BiRabittoh/fixyoutube-go/invidious"
 16	"github.com/gorilla/mux"
 17	"github.com/joho/godotenv"
 18)
 19
 20var templatesDirectory = "templates/"
 21var indexTemplate = template.Must(template.ParseFiles(templatesDirectory + "index.html"))
 22var videoTemplate = template.Must(template.ParseFiles(templatesDirectory + "video.html"))
 23var blacklist = []string{"favicon.ico", "robots.txt", "proxy"}
 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 apiKey string
 26
 27func indexHandler(w http.ResponseWriter, r *http.Request) {
 28	buf := &bytes.Buffer{}
 29	err := indexTemplate.Execute(buf, nil)
 30	if err != nil {
 31		http.Error(w, err.Error(), http.StatusInternalServerError)
 32		return
 33	}
 34
 35	buf.WriteTo(w)
 36}
 37
 38func clearHandler(w http.ResponseWriter, r *http.Request) {
 39	if r.Method != http.MethodPost {
 40		http.Redirect(w, r, "/", http.StatusMovedPermanently)
 41		return
 42	}
 43
 44	err := r.ParseForm()
 45	if err != nil {
 46		http.Error(w, err.Error(), http.StatusInternalServerError)
 47		return
 48	}
 49
 50	providedKey := r.PostForm.Get("apiKey")
 51	if providedKey != apiKey {
 52		http.Error(w, "Wrong or missing API key.", http.StatusForbidden)
 53		return
 54	}
 55
 56	invidious.ClearDB()
 57	http.Error(w, "Done.", http.StatusOK)
 58}
 59
 60func videoHandler(videoId string, formatIndex int, invidiousClient *invidious.Client, w http.ResponseWriter, r *http.Request) {
 61	userAgent := r.UserAgent()
 62	res := userAgentRegex.MatchString(userAgent)
 63	if !res {
 64		log.Println("Regex did not match. Redirecting. UA:", userAgent)
 65		url := "https://www.youtube.com/watch?v=" + videoId
 66		http.Redirect(w, r, url, http.StatusMovedPermanently)
 67		return
 68	}
 69
 70	video, err := invidiousClient.GetVideo(videoId)
 71	if err != nil {
 72		http.Error(w, err.Error(), http.StatusInternalServerError)
 73		return
 74	}
 75
 76	video.FormatIndex = formatIndex % len(video.Formats)
 77
 78	buf := &bytes.Buffer{}
 79	err = videoTemplate.Execute(buf, video)
 80	if err != nil {
 81		http.Error(w, err.Error(), http.StatusInternalServerError)
 82		return
 83	}
 84	buf.WriteTo(w)
 85}
 86
 87func watchHandler(invidiousClient *invidious.Client) http.HandlerFunc {
 88	return func(w http.ResponseWriter, r *http.Request) {
 89		u, err := url.Parse(r.URL.String())
 90		if err != nil {
 91			http.Error(w, err.Error(), http.StatusInternalServerError)
 92			return
 93		}
 94
 95		videoId := u.Query().Get("v")
 96		videoHandler(videoId, 0, invidiousClient, w, r)
 97	}
 98}
 99
100func parseFormatIndex(vars map[string]string) int {
101	formatIndex, err := strconv.Atoi(vars["formatIndex"])
102	if err != nil {
103		log.Println("Error: could not parse formatIndex.")
104		return 0
105	}
106	return formatIndex
107}
108
109func shortHandler(invidiousClient *invidious.Client) http.HandlerFunc {
110	return func(w http.ResponseWriter, r *http.Request) {
111		vars := mux.Vars(r)
112		videoId := vars["videoId"]
113		formatIndex := parseFormatIndex(vars)
114
115		if slices.Contains(blacklist, videoId) {
116			http.Error(w, "Not a valid ID.", http.StatusBadRequest)
117			return
118		}
119
120		videoHandler(videoId, formatIndex, invidiousClient, w, r)
121	}
122}
123
124func proxyHandler(invidiousClient *invidious.Client) http.HandlerFunc {
125	return func(w http.ResponseWriter, r *http.Request) {
126		vars := mux.Vars(r)
127		videoId := vars["videoId"]
128		formatIndex := parseFormatIndex(vars)
129		invidiousClient.ProxyVideo(w, videoId, formatIndex)
130	}
131}
132
133func main() {
134	err := godotenv.Load()
135	if err != nil {
136		log.Println("No .env file provided.")
137	}
138
139	port := os.Getenv("PORT")
140	if port == "" {
141		port = "3000"
142	}
143
144	apiKey = os.Getenv("API_KEY")
145	if apiKey == "" {
146		apiKey = "itsme"
147	}
148
149	myClient := &http.Client{Timeout: 10 * time.Second}
150	videoapi := invidious.NewClient(myClient)
151
152	r := mux.NewRouter()
153	r.HandleFunc("/", indexHandler)
154	r.HandleFunc("/clear", clearHandler)
155	r.HandleFunc("/watch", watchHandler(videoapi))
156	r.HandleFunc("/proxy/{videoId}", proxyHandler(videoapi))
157	r.HandleFunc("/proxy/{videoId}/{formatIndex}", proxyHandler(videoapi))
158	r.HandleFunc("/{videoId}", shortHandler(videoapi))
159	r.HandleFunc("/{videoId}/{formatIndex}", shortHandler(videoapi))
160	/*
161		// native go implementation (useless until february 2024)
162		r := http.NewServeMux()
163		r.HandleFunc("/watch", watchHandler(videoapi))
164		r.HandleFunc("/{videoId}/", shortHandler(videoapi))
165		r.HandleFunc("/", indexHandler)
166	*/
167	println("Serving on port", port)
168	http.ListenAndServe(":"+port, r)
169}