all repos — fixyoutube-go @ 48d10f3f94a07a0491c6dbd2bab6b28316162e66

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