fixyoutube.go (view raw)
1package main
2
3import (
4 "bytes"
5 "html/template"
6 "io"
7 "net/http"
8 "net/url"
9 "os"
10 "regexp"
11 "strconv"
12 "time"
13
14 "github.com/BiRabittoh/fixyoutube-go/invidious"
15 "github.com/gorilla/mux"
16 "github.com/joho/godotenv"
17 "github.com/sirupsen/logrus"
18)
19
20const templatesDirectory = "templates/"
21
22var logger = logrus.New()
23var indexTemplate = template.Must(template.ParseFiles(templatesDirectory + "index.html"))
24var videoTemplate = template.Must(template.ParseFiles(templatesDirectory + "video.html"))
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 indexHandler(w http.ResponseWriter, r *http.Request) {
31 buf := &bytes.Buffer{}
32 err := indexTemplate.Execute(buf, nil)
33 if err != nil {
34 logger.Error("Failed to fill index template.")
35 http.Error(w, err.Error(), http.StatusInternalServerError)
36 return
37 }
38
39 buf.WriteTo(w)
40}
41
42func clearHandler(w http.ResponseWriter, r *http.Request) {
43 if r.Method != http.MethodPost {
44 http.Redirect(w, r, "/", http.StatusMovedPermanently)
45 return
46 }
47
48 err := r.ParseForm()
49 if err != nil {
50 logger.Error("Failed to parse form in /clear.")
51 http.Error(w, err.Error(), http.StatusInternalServerError)
52 return
53 }
54
55 providedKey := r.PostForm.Get("apiKey")
56 if providedKey != apiKey {
57 logger.Debug("Wrong API key: ", providedKey)
58 http.Error(w, "Wrong or missing API key.", http.StatusForbidden)
59 return
60 }
61
62 invidious.ClearDB()
63 logger.Info("Cache cleared.")
64 http.Error(w, "Done.", http.StatusOK)
65}
66
67func videoHandler(videoId string, invidiousClient *invidious.Client, w http.ResponseWriter, r *http.Request) {
68 userAgent := r.UserAgent()
69 res := userAgentRegex.MatchString(userAgent)
70 if !res {
71 logger.Debug("Regex did not match. Redirecting. UA:", userAgent)
72 url := "https://www.youtube.com/watch?v=" + videoId
73 http.Redirect(w, r, url, http.StatusMovedPermanently)
74 return
75 }
76
77 if !videoRegex.MatchString(videoId) {
78 logger.Info("Invalid video ID: ", videoId)
79 http.Error(w, "Invalid video ID.", http.StatusBadRequest)
80 return
81 }
82
83 video, err := invidiousClient.GetVideo(videoId, true)
84 if err != nil {
85 logger.Info("Wrong video ID: ", videoId)
86 http.Error(w, "Wrong video ID.", http.StatusNotFound)
87 return
88 }
89
90 buf := &bytes.Buffer{}
91 err = videoTemplate.Execute(buf, video)
92 if err != nil {
93 logger.Error("Failed to fill video template.")
94 http.Error(w, err.Error(), http.StatusInternalServerError)
95 return
96 }
97 buf.WriteTo(w)
98}
99
100func watchHandler(invidiousClient *invidious.Client) http.HandlerFunc {
101 return func(w http.ResponseWriter, r *http.Request) {
102 u, err := url.Parse(r.URL.String())
103 if err != nil {
104 logger.Error("Failed to parse URL: ", r.URL.String())
105 http.Error(w, err.Error(), http.StatusInternalServerError)
106 return
107 }
108 q := u.Query()
109 videoId := q.Get("v")
110 videoHandler(videoId, invidiousClient, w, r)
111 }
112}
113
114func shortHandler(invidiousClient *invidious.Client) http.HandlerFunc {
115 return func(w http.ResponseWriter, r *http.Request) {
116 vars := mux.Vars(r)
117 videoId := vars["videoId"]
118 videoHandler(videoId, invidiousClient, w, r)
119 }
120}
121
122func proxyHandler(invidiousClient *invidious.Client) http.HandlerFunc {
123 return func(w http.ResponseWriter, r *http.Request) {
124 vars := mux.Vars(r)
125 videoId := vars["videoId"]
126
127 video, err := invidious.GetVideoDB(videoId)
128 if err != nil {
129 logger.Warn("Cannot proxy a video that is not cached: https://youtu.be/", videoId)
130 http.Error(w, "Something went wrong.", http.StatusBadRequest)
131 return
132 }
133
134 fmtAmount := len(video.Formats)
135
136 s := http.StatusNotFound
137 for i := fmtAmount - 1; i >= 0; i-- {
138 url := video.Formats[i].Url
139 logger.Debug(url)
140 b, l, httpStatus := invidiousClient.ProxyVideo(url)
141 if httpStatus == http.StatusOK {
142 h := w.Header()
143 h.Set("Status", "200")
144 h.Set("Content-Type", "video/mp4")
145 h.Set("Content-Length", strconv.FormatInt(l, 10))
146 io.Copy(w, b)
147 return
148 }
149 s = httpStatus
150 logger.Debug("Format ", i, "failed with status code ", s)
151 }
152
153 logger.Error("proxyHandler() failed. Final code: ", s)
154 http.Error(w, "Something went wrong.", s)
155 }
156}
157
158func main() {
159 //logger.SetLevel(logrus.DebugLevel)
160 err := godotenv.Load()
161 if err != nil {
162 logger.Info("No .env file provided.")
163 }
164
165 port := os.Getenv("PORT")
166 if port == "" {
167 port = "3000"
168 }
169
170 apiKey = os.Getenv("API_KEY")
171 if apiKey == "" {
172 apiKey = "itsme"
173 }
174
175 myClient := &http.Client{Timeout: 10 * time.Second}
176 videoapi := invidious.NewClient(myClient)
177
178 r := mux.NewRouter()
179 r.HandleFunc("/", indexHandler)
180 r.HandleFunc("/clear", clearHandler)
181 r.HandleFunc("/watch", watchHandler(videoapi))
182 r.HandleFunc("/proxy/{videoId}", proxyHandler(videoapi))
183 r.HandleFunc("/{videoId}", shortHandler(videoapi))
184 /*
185 // native go implementation
186 r := http.NewServeMux()
187 r.HandleFunc("/watch", watchHandler(videoapi))
188 r.HandleFunc("/{videoId}/", shortHandler(videoapi))
189 r.HandleFunc("/", indexHandler)
190 */
191 logger.Info("Serving on port ", port)
192 http.ListenAndServe(":"+port, r)
193}