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 parseFormat(f rabbitpipe.Format) (res string) {
36 isAudio := f.AudioChannels > 0
37
38 if isAudio {
39 bitrate, err := strconv.Atoi(f.Bitrate)
40 if err != nil {
41 logger.Error("Failed to convert bitrate to integer.")
42 return
43 }
44 res = strconv.Itoa(bitrate/1000) + "kbps"
45 } else {
46 res = f.Resolution
47 }
48
49 mime := strings.Split(f.Type, ";")
50 res += " - " + mime[0]
51
52 codecs := " (" + strings.Split(mime[1], "\"")[1] + ")"
53
54 if !isAudio {
55 res += fmt.Sprintf(" (%d FPS)", f.FPS)
56 }
57
58 res += codecs
59 return
60}
61
62func getItag(formats []rabbitpipe.Format, itag string) *rabbitpipe.Format {
63 for _, f := range formats {
64 if f.Itag == itag {
65 return &f
66 }
67 }
68
69 return nil
70}
71
72func indexHandler(w http.ResponseWriter, r *http.Request) {
73 err := indexTemplate.Execute(w, nil)
74 if err != nil {
75 logger.Error("Failed to fill index template.")
76 http.Error(w, err.Error(), http.StatusInternalServerError)
77 return
78 }
79}
80
81func videoHandler(videoID string, w http.ResponseWriter, r *http.Request) {
82 url := "https://www.youtube.com/watch?v=" + videoID
83
84 if !videoRegex.MatchString(videoID) {
85 logger.Info("Invalid video ID: ", videoID)
86 http.Error(w, "Invalid video ID.", http.StatusBadRequest)
87 return
88 }
89
90 video, err := invidious.RP.GetVideo(videoID)
91 if err != nil || video == nil {
92 logger.Info("Wrong video ID: ", videoID)
93 http.Error(w, "Wrong video ID.", http.StatusNotFound)
94 return
95 }
96
97 if invidious.GetVideoURL(*video) == "" {
98 logger.Debug("No URL available. Redirecting.")
99 http.Redirect(w, r, url, http.StatusFound)
100 return
101 }
102
103 err = videoTemplate.Execute(w, video)
104 if err != nil {
105 logger.Error("Failed to fill video template.")
106 http.Error(w, err.Error(), http.StatusInternalServerError)
107 return
108 }
109}
110
111func watchHandler(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
119 q := u.Query()
120 videoID := q.Get("v")
121 videoHandler(videoID, w, r)
122}
123
124func shortHandler(w http.ResponseWriter, r *http.Request) {
125 videoID := r.PathValue("videoID")
126 videoHandler(videoID, w, r)
127}
128
129func proxyHandler(w http.ResponseWriter, r *http.Request) {
130 videoID := r.PathValue("videoID")
131
132 vb, s := invidious.ProxyVideoId(videoID)
133 if s != http.StatusOK {
134 logger.Error("proxyHandler() failed. Final code: ", s)
135 http.Error(w, http.StatusText(s), s)
136 return
137 }
138 if !vb.ValidateLength() {
139 logger.Error("Buffer length is inconsistent.")
140 status := http.StatusInternalServerError
141 http.Error(w, http.StatusText(status), status)
142 return
143 }
144 h := w.Header()
145 h.Set("Status", "200")
146 h.Set("Content-Type", "video/mp4")
147 h.Set("Content-Length", strconv.FormatInt(vb.Length, 10))
148 io.Copy(w, vb.Buffer)
149}
150
151func downloadHandler(w http.ResponseWriter, r *http.Request) {
152 videoID := r.FormValue("video")
153 if videoID == "" {
154 http.Error(w, "Missing video ID", http.StatusBadRequest)
155 return
156 }
157
158 if !videoRegex.MatchString(videoID) {
159 logger.Println("Invalid video ID:", videoID)
160 http.Error(w, "not found", http.StatusNotFound)
161 return
162 }
163
164 itag := r.FormValue("itag")
165 if itag == "" {
166 http.Error(w, "not found", http.StatusBadRequest)
167 return
168 }
169
170 video, err := invidious.RP.GetVideo(videoID)
171 if err != nil || video == nil {
172 http.Error(w, "not found", http.StatusNotFound)
173 return
174 }
175
176 format := getItag(video.FormatStreams, itag)
177 if format == nil {
178 format = getItag(video.AdaptiveFormats, itag)
179 if format == nil {
180 http.Error(w, "not found", http.StatusNotFound)
181 return
182 }
183 }
184
185 http.Redirect(w, r, format.URL, http.StatusFound)
186}
187
188func refreshHandler(w http.ResponseWriter, r *http.Request) {
189 videoID := r.PathValue("videoID")
190 if videoID == "" {
191 http.Error(w, "bad request", http.StatusBadRequest)
192 return
193 }
194
195 if !videoRegex.MatchString(videoID) {
196 http.Error(w, "not found", http.StatusNotFound)
197 return
198 }
199
200 video, err := invidious.RP.GetVideoNoCache(videoID)
201 if err != nil || video == nil {
202 http.Error(w, "not found", http.StatusNotFound)
203 return
204 }
205
206 http.Redirect(w, r, "/"+videoID, http.StatusFound)
207}
208
209func cacheHandler(w http.ResponseWriter, r *http.Request) {
210 username, password, ok := r.BasicAuth()
211 if !ok || username != adminUser || password != adminPass {
212 w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
213 http.Error(w, "unauthorized", http.StatusUnauthorized)
214 return
215 }
216
217 var videos []rabbitpipe.Video
218 for s := range invidious.RP.GetCachedVideos() {
219 video, err := invidious.RP.GetVideo(s)
220 if err != nil || video == nil {
221 continue
222 }
223 videos = append(videos, *video)
224 }
225
226 err := cacheTemplate.Execute(w, videos)
227 if err != nil {
228 log.Println("cacheHandler ERROR:", err)
229 http.Error(w, "internal server error", http.StatusInternalServerError)
230 return
231 }
232}