handlers.go (view raw)
1package main
2
3import (
4 "embed"
5 "fmt"
6 "io"
7 "net/http"
8 "net/url"
9 "regexp"
10 "strconv"
11 "strings"
12 "text/template"
13
14 "github.com/birabittoh/fixyoutube-go/invidious"
15 "github.com/birabittoh/rabbitpipe"
16)
17
18const templatesDirectory = "templates/"
19
20var (
21 //go:embed templates/index.html templates/video.html
22 templates embed.FS
23 indexTemplate = template.Must(template.ParseFS(templates, templatesDirectory+"index.html"))
24 videoTemplate = template.Must(template.New("video.html").Funcs(template.FuncMap{"parseFormat": parseFormat}).ParseFS(templates, templatesDirectory+"video.html"))
25 // 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`)
26 videoRegex = regexp.MustCompile(`(?i)^[a-z0-9_-]{11}$`)
27)
28
29func parseFormat(f rabbitpipe.Format) (res string) {
30 isAudio := f.AudioChannels > 0
31
32 if isAudio {
33 bitrate, err := strconv.Atoi(f.Bitrate)
34 if err != nil {
35 logger.Error("Failed to convert bitrate to integer.")
36 return
37 }
38 res = strconv.Itoa(bitrate/1000) + "kbps"
39 } else {
40 res = f.Resolution
41 }
42
43 mime := strings.Split(f.Type, ";")
44 res += " - " + mime[0]
45
46 codecs := " (" + strings.Split(mime[1], "\"")[1] + ")"
47
48 if !isAudio {
49 res += fmt.Sprintf(" (%d FPS)", f.FPS)
50 }
51
52 res += codecs
53 return
54}
55
56func getItag(formats []rabbitpipe.Format, itag string) *rabbitpipe.Format {
57 for _, f := range formats {
58 if f.Itag == itag {
59 return &f
60 }
61 }
62
63 return nil
64}
65
66func indexHandler(w http.ResponseWriter, r *http.Request) {
67 err := indexTemplate.Execute(w, nil)
68 if err != nil {
69 logger.Error("Failed to fill index template.")
70 http.Error(w, err.Error(), http.StatusInternalServerError)
71 return
72 }
73}
74
75func videoHandler(videoID string, w http.ResponseWriter, r *http.Request) {
76 url := "https://www.youtube.com/watch?v=" + videoID
77
78 if !videoRegex.MatchString(videoID) {
79 logger.Info("Invalid video ID: ", videoID)
80 http.Error(w, "Invalid video ID.", http.StatusBadRequest)
81 return
82 }
83
84 video, err := invidious.RP.GetVideo(videoID)
85 if err != nil || video == nil {
86 logger.Info("Wrong video ID: ", videoID)
87 http.Error(w, "Wrong video ID.", http.StatusNotFound)
88 return
89 }
90
91 if invidious.GetVideoURL(*video) == "" {
92 logger.Debug("No URL available. Redirecting.")
93 http.Redirect(w, r, url, http.StatusFound)
94 return
95 }
96
97 err = videoTemplate.Execute(w, video)
98 if err != nil {
99 logger.Error("Failed to fill video template.")
100 http.Error(w, err.Error(), http.StatusInternalServerError)
101 return
102 }
103}
104
105func watchHandler(w http.ResponseWriter, r *http.Request) {
106 u, err := url.Parse(r.URL.String())
107 if err != nil {
108 logger.Error("Failed to parse URL: ", r.URL.String())
109 http.Error(w, err.Error(), http.StatusInternalServerError)
110 return
111 }
112
113 q := u.Query()
114 videoID := q.Get("v")
115 videoHandler(videoID, w, r)
116}
117
118func shortHandler(w http.ResponseWriter, r *http.Request) {
119 videoID := r.PathValue("videoID")
120 videoHandler(videoID, w, r)
121}
122
123func proxyHandler(w http.ResponseWriter, r *http.Request) {
124 videoID := r.PathValue("videoID")
125
126 vb, s := invidious.ProxyVideoId(videoID)
127 if s != http.StatusOK {
128 logger.Error("proxyHandler() failed. Final code: ", s)
129 http.Error(w, http.StatusText(s), s)
130 return
131 }
132 if !vb.ValidateLength() {
133 logger.Error("Buffer length is inconsistent.")
134 status := http.StatusInternalServerError
135 http.Error(w, http.StatusText(status), status)
136 return
137 }
138 h := w.Header()
139 h.Set("Status", "200")
140 h.Set("Content-Type", "video/mp4")
141 h.Set("Content-Length", strconv.FormatInt(vb.Length, 10))
142 io.Copy(w, vb.Buffer)
143}
144
145func downloadHandler(w http.ResponseWriter, r *http.Request) {
146 videoID := r.FormValue("video")
147 if videoID == "" {
148 http.Error(w, "Missing video ID", http.StatusBadRequest)
149 return
150 }
151
152 if !videoRegex.MatchString(videoID) {
153 logger.Println("Invalid video ID:", videoID)
154 http.Error(w, "not found", http.StatusNotFound)
155 return
156 }
157
158 itag := r.FormValue("itag")
159 if itag == "" {
160 http.Error(w, "not found", http.StatusBadRequest)
161 return
162 }
163
164 video, err := invidious.RP.GetVideo(videoID)
165 if err != nil || video == nil {
166 http.Error(w, "not found", http.StatusNotFound)
167 return
168 }
169
170 format := getItag(video.FormatStreams, itag)
171 if format == nil {
172 format = getItag(video.AdaptiveFormats, itag)
173 if format == nil {
174 http.Error(w, "not found", http.StatusNotFound)
175 return
176 }
177 }
178
179 http.Redirect(w, r, format.URL, http.StatusFound)
180}
181
182func refreshHandler(w http.ResponseWriter, r *http.Request) {
183 videoID := r.PathValue("videoID")
184 if videoID == "" {
185 http.Error(w, "bad request", http.StatusBadRequest)
186 return
187 }
188
189 if !videoRegex.MatchString(videoID) {
190 http.Error(w, "not found", http.StatusNotFound)
191 return
192 }
193
194 video, err := invidious.RP.GetVideoNoCache(videoID)
195 if err != nil || video == nil {
196 http.Error(w, "not found", http.StatusNotFound)
197 return
198 }
199
200 http.Redirect(w, r, "/"+videoID, http.StatusFound)
201}