package main
import (
"bytes"
"embed"
"html/template"
"io"
"net/http"
"net/url"
"os"
"regexp"
"strconv"
"time"
"github.com/BiRabittoh/fixyoutube-go/invidious"
"github.com/joho/godotenv"
"github.com/sirupsen/logrus"
)
const templatesDirectory = "templates/"
var (
//go:embed templates/index.html templates/video.html
templates embed.FS
apiKey string
logger = logrus.New()
indexTemplate = template.Must(template.ParseFS(templates, templatesDirectory+"index.html"))
videoTemplate = template.Must(template.ParseFS(templates, templatesDirectory+"video.html"))
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`)
videoRegex = regexp.MustCompile(`(?i)^[a-z0-9_-]{11}$`)
)
func indexHandler(w http.ResponseWriter, r *http.Request) {
buf := &bytes.Buffer{}
err := indexTemplate.Execute(buf, nil)
if err != nil {
logger.Error("Failed to fill index template.")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
buf.WriteTo(w)
}
func clearHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Redirect(w, r, "/", http.StatusMovedPermanently)
return
}
err := r.ParseForm()
if err != nil {
logger.Error("Failed to parse form in /clear.")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
providedKey := r.PostForm.Get("apiKey")
if providedKey != apiKey {
logger.Debug("Wrong API key: ", providedKey)
http.Error(w, "Wrong or missing API key.", http.StatusForbidden)
return
}
invidious.ClearDB()
logger.Info("Cache cleared.")
http.Error(w, "Done.", http.StatusOK)
}
func videoHandler(videoId string, invidiousClient *invidious.Client, w http.ResponseWriter, r *http.Request) {
url := "https://www.youtube.com/watch?v=" + videoId
userAgent := r.UserAgent()
res := userAgentRegex.MatchString(userAgent)
if !res {
logger.Debug("Regex did not match. Redirecting. UA:", userAgent)
http.Redirect(w, r, url, http.StatusMovedPermanently)
return
}
if !videoRegex.MatchString(videoId) {
logger.Info("Invalid video ID: ", videoId)
http.Error(w, "Invalid video ID.", http.StatusBadRequest)
return
}
video, err := invidiousClient.GetVideo(videoId, true)
if err != nil {
logger.Info("Wrong video ID: ", videoId)
http.Error(w, "Wrong video ID.", http.StatusNotFound)
return
}
if video.Url == "" {
logger.Debug("No URL available. Redirecting.")
http.Redirect(w, r, url, http.StatusMovedPermanently)
return
}
buf := &bytes.Buffer{}
err = videoTemplate.Execute(buf, video)
if err != nil {
logger.Error("Failed to fill video template.")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
buf.WriteTo(w)
}
func watchHandler(invidiousClient *invidious.Client) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
u, err := url.Parse(r.URL.String())
if err != nil {
logger.Error("Failed to parse URL: ", r.URL.String())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
q := u.Query()
videoId := q.Get("v")
videoHandler(videoId, invidiousClient, w, r)
}
}
func shortHandler(invidiousClient *invidious.Client) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
videoId := r.PathValue("videoId")
videoHandler(videoId, invidiousClient, w, r)
}
}
func proxyHandler(invidiousClient *invidious.Client) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
videoId := r.PathValue("videoId")
vb, s := invidiousClient.ProxyVideoId(videoId)
if s != http.StatusOK {
logger.Error("proxyHandler() failed. Final code: ", s)
http.Error(w, "Something went wrong.", s)
return
}
if !vb.ValidateLength() {
logger.Error("Buffer length is inconsistent.")
http.Error(w, "Something went wrong.", http.StatusInternalServerError)
return
}
h := w.Header()
h.Set("Status", "200")
h.Set("Content-Type", "video/mp4")
h.Set("Content-Length", strconv.FormatInt(vb.Length, 10))
io.Copy(w, vb.Buffer)
}
}
func getenvDefault(key string, def string) string {
res := os.Getenv(key)
if res == "" {
return def
}
return res
}
func getenvDefaultParse(key string, def string) float64 {
value := getenvDefault(key, def)
res, err := strconv.ParseFloat(value, 64)
if err != nil {
logger.Fatal(err)
}
return res
}
func main() {
logger.SetLevel(logrus.DebugLevel)
err := godotenv.Load()
if err != nil {
logger.Info("No .env file provided.")
}
apiKey = getenvDefault("API_KEY", "itsme")
port := getenvDefault("PORT", "3000")
cacheDuration := getenvDefaultParse("CACHE_DURATION_MINUTES", "5")
timeoutDuration := getenvDefaultParse("TIMEOUT_DURATION_MINUTES", "10")
cleanupInterval := getenvDefaultParse("CLEANUP_INTERVAL_SECONDS", "30")
maxSizeMB := getenvDefaultParse("MAX_SIZE_MB", "20")
myClient := &http.Client{Timeout: 10 * time.Second}
options := invidious.ClientOptions{
CacheDuration: time.Duration(cacheDuration) * time.Minute,
TimeoutDuration: time.Duration(timeoutDuration) * time.Minute,
CleanupInterval: time.Duration(cleanupInterval) * time.Second,
MaxSizeBytes: int64(maxSizeMB * 1000000),
}
videoapi := invidious.NewClient(myClient, options)
r := http.NewServeMux()
r.HandleFunc("/", indexHandler)
r.HandleFunc("/clear", clearHandler)
r.HandleFunc("/watch", watchHandler(videoapi))
r.HandleFunc("/proxy/{videoId}", proxyHandler(videoapi))
r.HandleFunc("/{videoId}", shortHandler(videoapi))
/*
// native go implementation
r := http.NewServeMux()
r.HandleFunc("/watch", watchHandler(videoapi))
r.HandleFunc("/{videoId}/", shortHandler(videoapi))
r.HandleFunc("/", indexHandler)
*/
logger.Info("Serving on port ", port)
http.ListenAndServe(":"+port, r)
}