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