fixyoutube.go (view raw)
1package main
2
3import (
4 "bytes"
5 "embed"
6 "html/template"
7 "io"
8 "net/http"
9 "net/url"
10 "os"
11 "regexp"
12 "strconv"
13 "time"
14
15 "github.com/BiRabittoh/fixyoutube-go/invidious"
16 "github.com/gorilla/mux"
17 "github.com/joho/godotenv"
18 "github.com/sirupsen/logrus"
19)
20
21const templatesDirectory = "templates/"
22
23var (
24 //go:embed templates/index.html templates/video.html
25 templates embed.FS
26 apiKey string
27 logger = logrus.New()
28 indexTemplate = template.Must(template.ParseFS(templates, templatesDirectory+"index.html"))
29 videoTemplate = template.Must(template.ParseFS(templates, templatesDirectory+"video.html"))
30 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`)
31 videoRegex = regexp.MustCompile(`(?i)^[a-z0-9_-]{11}$`)
32)
33
34func indexHandler(w http.ResponseWriter, r *http.Request) {
35 buf := &bytes.Buffer{}
36 err := indexTemplate.Execute(buf, nil)
37 if err != nil {
38 logger.Error("Failed to fill index template.")
39 http.Error(w, err.Error(), http.StatusInternalServerError)
40 return
41 }
42
43 buf.WriteTo(w)
44}
45
46func clearHandler(w http.ResponseWriter, r *http.Request) {
47 if r.Method != http.MethodPost {
48 http.Redirect(w, r, "/", http.StatusMovedPermanently)
49 return
50 }
51
52 err := r.ParseForm()
53 if err != nil {
54 logger.Error("Failed to parse form in /clear.")
55 http.Error(w, err.Error(), http.StatusInternalServerError)
56 return
57 }
58
59 providedKey := r.PostForm.Get("apiKey")
60 if providedKey != apiKey {
61 logger.Debug("Wrong API key: ", providedKey)
62 http.Error(w, "Wrong or missing API key.", http.StatusForbidden)
63 return
64 }
65
66 invidious.ClearDB()
67 logger.Info("Cache cleared.")
68 http.Error(w, "Done.", http.StatusOK)
69}
70
71func videoHandler(videoId string, invidiousClient *invidious.Client, w http.ResponseWriter, r *http.Request) {
72 url := "https://www.youtube.com/watch?v=" + videoId
73 userAgent := r.UserAgent()
74 res := userAgentRegex.MatchString(userAgent)
75 if !res {
76 logger.Debug("Regex did not match. Redirecting. UA:", userAgent)
77 http.Redirect(w, r, url, http.StatusMovedPermanently)
78 return
79 }
80
81 if !videoRegex.MatchString(videoId) {
82 logger.Info("Invalid video ID: ", videoId)
83 http.Error(w, "Invalid video ID.", http.StatusBadRequest)
84 return
85 }
86
87 video, err := invidiousClient.GetVideo(videoId, true)
88 if err != nil {
89 logger.Info("Wrong video ID: ", videoId)
90 http.Error(w, "Wrong video ID.", http.StatusNotFound)
91 return
92 }
93
94 if video.Url == "" {
95 logger.Debug("No URL available. Redirecting.")
96 http.Redirect(w, r, url, http.StatusMovedPermanently)
97 return
98 }
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 videoHandler(videoId, invidiousClient, w, r)
121 }
122}
123
124func shortHandler(invidiousClient *invidious.Client) http.HandlerFunc {
125 return func(w http.ResponseWriter, r *http.Request) {
126 vars := mux.Vars(r)
127 videoId := vars["videoId"]
128 videoHandler(videoId, invidiousClient, w, r)
129 }
130}
131
132func proxyHandler(invidiousClient *invidious.Client) http.HandlerFunc {
133 return func(w http.ResponseWriter, r *http.Request) {
134 vars := mux.Vars(r)
135 videoId := vars["videoId"]
136
137 vb, s := invidiousClient.ProxyVideoId(videoId)
138 if s != http.StatusOK {
139 logger.Error("proxyHandler() failed. Final code: ", s)
140 http.Error(w, "Something went wrong.", s)
141 return
142 }
143 if !vb.ValidateLength() {
144 logger.Error("Buffer length is inconsistent.")
145 http.Error(w, "Something went wrong.", http.StatusInternalServerError)
146 return
147 }
148 h := w.Header()
149 h.Set("Status", "200")
150 h.Set("Content-Type", "video/mp4")
151 h.Set("Content-Length", strconv.FormatInt(vb.Length, 10))
152 io.Copy(w, vb.Buffer)
153 }
154}
155
156func getenvDefault(key string, def string) string {
157 res := os.Getenv(key)
158 if res == "" {
159 return def
160 }
161 return res
162}
163
164func getenvDefaultParse(key string, def string) float64 {
165 value := getenvDefault(key, def)
166 res, err := strconv.ParseFloat(value, 64)
167 if err != nil {
168 logger.Fatal(err)
169 }
170 return res
171}
172
173func main() {
174 logger.SetLevel(logrus.DebugLevel)
175 err := godotenv.Load()
176 if err != nil {
177 logger.Info("No .env file provided.")
178 }
179
180 apiKey = getenvDefault("API_KEY", "itsme")
181 port := getenvDefault("PORT", "3000")
182 cacheDuration := getenvDefaultParse("CACHE_DURATION_MINUTES", "5")
183 timeoutDuration := getenvDefaultParse("TIMEOUT_DURATION_MINUTES", "10")
184 maxSizeMB := getenvDefaultParse("MAX_SIZE_MB", "20")
185
186 myClient := &http.Client{Timeout: 10 * time.Second}
187 options := invidious.ClientOptions{
188 CacheDuration: time.Duration(cacheDuration) * time.Minute,
189 TimeoutDuration: time.Duration(timeoutDuration) * time.Minute,
190 MaxSizeBytes: int64(maxSizeMB * 1000000),
191 }
192 videoapi := invidious.NewClient(myClient, options)
193
194 r := mux.NewRouter()
195 r.HandleFunc("/", indexHandler)
196 r.HandleFunc("/clear", clearHandler)
197 r.HandleFunc("/watch", watchHandler(videoapi))
198 r.HandleFunc("/proxy/{videoId}", proxyHandler(videoapi))
199 r.HandleFunc("/{videoId}", shortHandler(videoapi))
200 /*
201 // native go implementation
202 r := http.NewServeMux()
203 r.HandleFunc("/watch", watchHandler(videoapi))
204 r.HandleFunc("/{videoId}/", shortHandler(videoapi))
205 r.HandleFunc("/", indexHandler)
206 */
207 logger.Info("Serving on port ", port)
208 http.ListenAndServe(":"+port, r)
209}