handlers.go (view raw)
1package main
2
3import (
4 "embed"
5 "fmt"
6 "io"
7 "log"
8 "net/http"
9 "net/url"
10 "regexp"
11 "strconv"
12 "strings"
13 "text/template"
14
15 "github.com/birabittoh/fixyoutube-go/invidious"
16 "github.com/birabittoh/rabbitpipe"
17)
18
19const templatesDirectory = "templates/"
20
21var (
22 //go:embed templates/index.html templates/video.html templates/cache.html
23 templates embed.FS
24 indexTemplate = template.Must(template.ParseFS(templates, templatesDirectory+"index.html"))
25 videoTemplate = template.Must(template.New("video.html").Funcs(template.FuncMap{"parseFormat": parseFormat}).ParseFS(templates, templatesDirectory+"video.html"))
26 cacheTemplate = template.Must(template.New("cache.html").ParseFS(templates, templatesDirectory+"cache.html"))
27
28 // 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`)
29 videoRegex = regexp.MustCompile(`(?i)^[a-z0-9_-]{11}$`)
30
31 adminUser string
32 adminPass string
33)
34
35func defaultError(w http.ResponseWriter, code int) {
36 http.Error(w, http.StatusText(code), code)
37}
38
39func parseFormat(f rabbitpipe.Format) (res string) {
40 isAudio := f.AudioChannels > 0
41
42 if isAudio {
43 bitrate, err := strconv.Atoi(f.Bitrate)
44 if err != nil {
45 logger.Error("Failed to convert bitrate to integer.")
46 return
47 }
48 res = strconv.Itoa(bitrate/1000) + "kbps"
49 } else {
50 res = f.Resolution
51 }
52
53 mime := strings.Split(f.Type, ";")
54 res += " - " + mime[0]
55
56 codecs := " (" + strings.Split(mime[1], "\"")[1] + ")"
57
58 if !isAudio {
59 res += fmt.Sprintf(" (%d FPS)", f.FPS)
60 }
61
62 res += codecs
63 return
64}
65
66func getItag(formats []rabbitpipe.Format, itag string) *rabbitpipe.Format {
67 for _, f := range formats {
68 if f.Itag == itag {
69 return &f
70 }
71 }
72
73 return nil
74}
75
76func indexHandler(w http.ResponseWriter, r *http.Request) {
77 err := indexTemplate.Execute(w, nil)
78 if err != nil {
79 logger.Error("Failed to fill index template.")
80 defaultError(w, http.StatusInternalServerError)
81 return
82 }
83}
84
85func videoHandler(videoID string, w http.ResponseWriter, r *http.Request) {
86 url := "https://www.youtube.com/watch?v=" + videoID
87
88 if !videoRegex.MatchString(videoID) {
89 logger.Info("Invalid video ID: ", videoID)
90 http.Error(w, "Invalid video ID.", http.StatusBadRequest)
91 return
92 }
93
94 video, err := invidious.RP.GetVideo(videoID)
95 if err != nil || video == nil {
96 logger.Info("Wrong video ID: ", videoID)
97 http.Error(w, "Wrong video ID.", http.StatusNotFound)
98 return
99 }
100
101 if invidious.GetVideoURL(*video) == "" {
102 logger.Debug("No URL available. Redirecting.")
103 http.Redirect(w, r, url, http.StatusFound)
104 return
105 }
106
107 err = videoTemplate.Execute(w, video)
108 if err != nil {
109 logger.Error("Failed to fill video template.")
110 http.Error(w, err.Error(), http.StatusInternalServerError)
111 return
112 }
113}
114
115func watchHandler(w http.ResponseWriter, r *http.Request) {
116 u, err := url.Parse(r.URL.String())
117 if err != nil {
118 logger.Error("Failed to parse URL: ", r.URL.String())
119 http.Error(w, err.Error(), http.StatusInternalServerError)
120 return
121 }
122
123 q := u.Query()
124 videoID := q.Get("v")
125 videoHandler(videoID, w, r)
126}
127
128func shortHandler(w http.ResponseWriter, r *http.Request) {
129 videoID := r.PathValue("videoID")
130 videoHandler(videoID, w, r)
131}
132
133func proxyHandler(w http.ResponseWriter, r *http.Request) {
134 videoID := r.PathValue("videoID")
135
136 vb, s := invidious.ProxyVideoId(videoID)
137 if s != http.StatusOK {
138 logger.Error("proxyHandler() failed. Final code: ", s)
139 http.Error(w, http.StatusText(s), s)
140 return
141 }
142 if !vb.ValidateLength() {
143 logger.Error("Buffer length is inconsistent.")
144 status := http.StatusInternalServerError
145 http.Error(w, http.StatusText(status), status)
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
155func subHandler(w http.ResponseWriter, r *http.Request) {
156 videoID := r.PathValue("videoID")
157 language := r.PathValue("language")
158
159 captions, err := invidious.RP.GetCaptions(videoID, language)
160 if err != nil {
161 logger.Error("Failed to get captions: ", err)
162 defaultError(w, http.StatusNotFound)
163 return
164 }
165
166 w.Header().Set("Content-Type", "text/vtt")
167 w.Header().Set("Content-Length", strconv.Itoa(len(captions)))
168
169 _, err = w.Write(captions)
170 if err != nil {
171 defaultError(w, http.StatusInternalServerError)
172 return
173 }
174}
175
176func downloadHandler(w http.ResponseWriter, r *http.Request) {
177 videoID := r.FormValue("video")
178 if videoID == "" {
179 http.Error(w, "Missing video ID", http.StatusBadRequest)
180 return
181 }
182
183 if !videoRegex.MatchString(videoID) {
184 logger.Println("Invalid video ID:", videoID)
185 http.Error(w, "not found", http.StatusNotFound)
186 return
187 }
188
189 itag := r.FormValue("itag")
190 if itag == "" {
191 http.Error(w, "not found", http.StatusBadRequest)
192 return
193 }
194
195 video, err := invidious.RP.GetVideo(videoID)
196 if err != nil || video == nil {
197 http.Error(w, "not found", http.StatusNotFound)
198 return
199 }
200
201 format := getItag(video.FormatStreams, itag)
202 if format == nil {
203 format = getItag(video.AdaptiveFormats, itag)
204 if format == nil {
205 http.Error(w, "not found", http.StatusNotFound)
206 return
207 }
208 }
209
210 http.Redirect(w, r, format.URL, http.StatusFound)
211}
212
213func refreshHandler(w http.ResponseWriter, r *http.Request) {
214 videoID := r.PathValue("videoID")
215 if videoID == "" {
216 http.Error(w, "bad request", http.StatusBadRequest)
217 return
218 }
219
220 if !videoRegex.MatchString(videoID) {
221 http.Error(w, "not found", http.StatusNotFound)
222 return
223 }
224
225 video, err := invidious.RP.GetVideoNoCache(videoID)
226 if err != nil || video == nil {
227 http.Error(w, "not found", http.StatusNotFound)
228 return
229 }
230
231 http.Redirect(w, r, "/"+videoID, http.StatusFound)
232}
233
234func cacheHandler(w http.ResponseWriter, r *http.Request) {
235 username, password, ok := r.BasicAuth()
236 if !ok || username != adminUser || password != adminPass {
237 w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
238 http.Error(w, "unauthorized", http.StatusUnauthorized)
239 return
240 }
241
242 var videos []rabbitpipe.Video
243 for s := range invidious.RP.GetCachedVideos() {
244 video, err := invidious.RP.GetVideo(s)
245 if err != nil || video == nil {
246 continue
247 }
248 videos = append(videos, *video)
249 }
250
251 err := cacheTemplate.Execute(w, videos)
252 if err != nil {
253 log.Println("cacheHandler ERROR:", err)
254 http.Error(w, "internal server error", http.StatusInternalServerError)
255 return
256 }
257}