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