all repos — fixyoutube-go @ 1ed34f89fd58b854daee23c3e8188e071ff5a852

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
 20const templatesDirectory = "templates/"
 21
 22var indexTemplate = template.Must(template.ParseFiles(templatesDirectory + "index.html"))
 23var videoTemplate = template.Must(template.ParseFiles(templatesDirectory + "video.html"))
 24var blacklist = []string{"favicon.ico", "robots.txt", "proxy"}
 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		log.Println("Error: 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		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		http.Error(w, err.Error(), http.StatusInternalServerError)
 59		return
 60	}
 61
 62	providedKey := r.PostForm.Get("apiKey")
 63	if providedKey != apiKey {
 64		http.Error(w, "Wrong or missing API key.", http.StatusForbidden)
 65		return
 66	}
 67
 68	invidious.ClearDB()
 69	http.Error(w, "Done.", http.StatusOK)
 70}
 71
 72func videoHandler(videoId string, formatIndex int, invidiousClient *invidious.Client, w http.ResponseWriter, r *http.Request) {
 73	userAgent := r.UserAgent()
 74	res := userAgentRegex.MatchString(userAgent)
 75	if !res {
 76		log.Println("Regex did not match. Redirecting. UA:", userAgent)
 77		url := "https://www.youtube.com/watch?v=" + videoId
 78		http.Redirect(w, r, url, http.StatusMovedPermanently)
 79		return
 80	}
 81
 82	if !videoRegex.MatchString(videoId) {
 83		http.Error(w, "Bad Video ID.", http.StatusBadRequest)
 84		return
 85	}
 86
 87	video, err := invidiousClient.GetVideo(videoId)
 88	if err != nil {
 89		http.Error(w, "Wrong Video ID.", http.StatusNotFound)
 90		return
 91	}
 92
 93	video.FormatIndex = formatIndex % len(video.Formats)
 94
 95	buf := &bytes.Buffer{}
 96	err = videoTemplate.Execute(buf, video)
 97	if err != nil {
 98		http.Error(w, err.Error(), http.StatusInternalServerError)
 99		return
100	}
101	buf.WriteTo(w)
102}
103
104func watchHandler(invidiousClient *invidious.Client) http.HandlerFunc {
105	return func(w http.ResponseWriter, r *http.Request) {
106		u, err := url.Parse(r.URL.String())
107		if err != nil {
108			http.Error(w, err.Error(), http.StatusInternalServerError)
109			return
110		}
111		q := u.Query()
112		videoId := q.Get("v")
113		formatIndex := parseFormatIndex(q.Get("f"))
114		videoHandler(videoId, formatIndex, invidiousClient, w, r)
115	}
116}
117
118func shortHandler(invidiousClient *invidious.Client) http.HandlerFunc {
119	return func(w http.ResponseWriter, r *http.Request) {
120		vars := mux.Vars(r)
121		videoId := vars["videoId"]
122		formatIndex := parseFormatIndex(vars["formatIndex"])
123
124		if slices.Contains(blacklist, videoId) {
125			http.Error(w, "Not a valid ID.", http.StatusBadRequest)
126			return
127		}
128
129		videoHandler(videoId, formatIndex, invidiousClient, w, r)
130	}
131}
132
133func proxyHandler(invidiousClient *invidious.Client) http.HandlerFunc {
134	return func(w http.ResponseWriter, r *http.Request) {
135		vars := mux.Vars(r)
136		videoId := vars["videoId"]
137		formatIndex := parseFormatIndex(vars["formatIndex"])
138		invidiousClient.ProxyVideo(w, videoId, formatIndex)
139	}
140}
141
142func main() {
143	err := godotenv.Load()
144	if err != nil {
145		log.Println("No .env file provided.")
146	}
147
148	port := os.Getenv("PORT")
149	if port == "" {
150		port = "3000"
151	}
152
153	apiKey = os.Getenv("API_KEY")
154	if apiKey == "" {
155		apiKey = "itsme"
156	}
157
158	myClient := &http.Client{Timeout: 10 * time.Second}
159	videoapi := invidious.NewClient(myClient)
160
161	r := mux.NewRouter()
162	r.HandleFunc("/", indexHandler)
163	r.HandleFunc("/clear", clearHandler)
164	r.HandleFunc("/watch", watchHandler(videoapi))
165	r.HandleFunc("/proxy/{videoId}", proxyHandler(videoapi))
166	r.HandleFunc("/proxy/{videoId}/{formatIndex}", proxyHandler(videoapi))
167	r.HandleFunc("/{videoId}", shortHandler(videoapi))
168	r.HandleFunc("/{videoId}/{formatIndex}", shortHandler(videoapi))
169	/*
170		// native go implementation (useless until february 2024)
171		r := http.NewServeMux()
172		r.HandleFunc("/watch", watchHandler(videoapi))
173		r.HandleFunc("/{videoId}/", shortHandler(videoapi))
174		r.HandleFunc("/", indexHandler)
175	*/
176	println("Serving on port", port)
177	http.ListenAndServe(":"+port, r)
178}