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