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}