fixyoutube.go (view raw)
1package main
2
3import (
4 "bytes"
5 "html/template"
6 "net/http"
7 "net/url"
8 "os"
9 "regexp"
10 "strconv"
11 "time"
12
13 "github.com/BiRabittoh/fixyoutube-go/invidious"
14 "github.com/gorilla/mux"
15 "github.com/joho/godotenv"
16 "github.com/sirupsen/logrus"
17)
18
19const templatesDirectory = "templates/"
20
21var logger = logrus.New()
22var indexTemplate = template.Must(template.ParseFiles(templatesDirectory + "index.html"))
23var videoTemplate = template.Must(template.ParseFiles(templatesDirectory + "video.html"))
24var 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`)
25var videoRegex = regexp.MustCompile(`^(?i)[a-z0-9_-]{11}$`)
26
27var apiKey string
28
29func parseFormatIndex(formatIndexString string) int {
30 formatIndex, err := strconv.Atoi(formatIndexString)
31 if err != nil || formatIndex < 0 {
32 logger.Debug("Could not parse formatIndex.")
33 return 0
34 }
35 return formatIndex
36}
37
38func indexHandler(w http.ResponseWriter, r *http.Request) {
39 buf := &bytes.Buffer{}
40 err := indexTemplate.Execute(buf, nil)
41 if err != nil {
42 logger.Error("Failed to fill index template.")
43 http.Error(w, err.Error(), http.StatusInternalServerError)
44 return
45 }
46
47 buf.WriteTo(w)
48}
49
50func clearHandler(w http.ResponseWriter, r *http.Request) {
51 if r.Method != http.MethodPost {
52 http.Redirect(w, r, "/", http.StatusMovedPermanently)
53 return
54 }
55
56 err := r.ParseForm()
57 if err != nil {
58 logger.Error("Failed to parse form in /clear.")
59 http.Error(w, err.Error(), http.StatusInternalServerError)
60 return
61 }
62
63 providedKey := r.PostForm.Get("apiKey")
64 if providedKey != apiKey {
65 logger.Debug("Wrong API key: ", providedKey)
66 http.Error(w, "Wrong or missing API key.", http.StatusForbidden)
67 return
68 }
69
70 invidious.ClearDB()
71 logger.Info("Cache cleared.")
72 http.Error(w, "Done.", http.StatusOK)
73}
74
75func videoHandler(videoId string, formatIndex int, invidiousClient *invidious.Client, w http.ResponseWriter, r *http.Request) {
76 userAgent := r.UserAgent()
77 res := userAgentRegex.MatchString(userAgent)
78 if !res {
79 logger.Debug("Regex did not match. Redirecting. UA:", userAgent)
80 url := "https://www.youtube.com/watch?v=" + videoId
81 http.Redirect(w, r, url, http.StatusMovedPermanently)
82 return
83 }
84
85 if !videoRegex.MatchString(videoId) {
86 logger.Info("Invalid video ID: ", videoId)
87 http.Error(w, "Invalid video ID.", http.StatusBadRequest)
88 return
89 }
90
91 video, err := invidiousClient.GetVideo(videoId)
92 if err != nil {
93 logger.Info("Wrong video ID: ", videoId)
94 http.Error(w, "Wrong video ID.", http.StatusNotFound)
95 return
96 }
97
98 video.FormatIndex = formatIndex % len(video.Formats)
99
100 buf := &bytes.Buffer{}
101 err = videoTemplate.Execute(buf, video)
102 if err != nil {
103 logger.Error("Failed to fill video template.")
104 http.Error(w, err.Error(), http.StatusInternalServerError)
105 return
106 }
107 buf.WriteTo(w)
108}
109
110func watchHandler(invidiousClient *invidious.Client) http.HandlerFunc {
111 return func(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 q := u.Query()
119 videoId := q.Get("v")
120 formatIndex := parseFormatIndex(q.Get("f"))
121 videoHandler(videoId, formatIndex, invidiousClient, w, r)
122 }
123}
124
125func shortHandler(invidiousClient *invidious.Client) http.HandlerFunc {
126 return func(w http.ResponseWriter, r *http.Request) {
127 vars := mux.Vars(r)
128 videoId := vars["videoId"]
129 formatIndex := parseFormatIndex(vars["formatIndex"])
130 videoHandler(videoId, formatIndex, invidiousClient, w, r)
131 }
132}
133
134func proxyHandler(invidiousClient *invidious.Client) http.HandlerFunc {
135 return func(w http.ResponseWriter, r *http.Request) {
136 vars := mux.Vars(r)
137 videoId := vars["videoId"]
138 formatIndex := parseFormatIndex(vars["formatIndex"])
139 invidiousClient.ProxyVideo(w, videoId, formatIndex)
140 }
141}
142
143func main() {
144 err := godotenv.Load()
145 if err != nil {
146 logger.Info("No .env file provided.")
147 }
148
149 port := os.Getenv("PORT")
150 if port == "" {
151 port = "3000"
152 }
153
154 apiKey = os.Getenv("API_KEY")
155 if apiKey == "" {
156 apiKey = "itsme"
157 }
158
159 myClient := &http.Client{Timeout: 10 * time.Second}
160 videoapi := invidious.NewClient(myClient)
161
162 r := mux.NewRouter()
163 r.HandleFunc("/", indexHandler)
164 r.HandleFunc("/clear", clearHandler)
165 r.HandleFunc("/watch", watchHandler(videoapi))
166 r.HandleFunc("/proxy/{videoId}", proxyHandler(videoapi))
167 r.HandleFunc("/proxy/{videoId}/{formatIndex}", proxyHandler(videoapi))
168 r.HandleFunc("/{videoId}", shortHandler(videoapi))
169 r.HandleFunc("/{videoId}/{formatIndex}", shortHandler(videoapi))
170 /*
171 // native go implementation (useless until february 2024)
172 r := http.NewServeMux()
173 r.HandleFunc("/watch", watchHandler(videoapi))
174 r.HandleFunc("/{videoId}/", shortHandler(videoapi))
175 r.HandleFunc("/", indexHandler)
176 */
177 logger.Info("Serving on port ", port)
178 http.ListenAndServe(":"+port, r)
179}