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