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