diff --git a/web/formats.go b/app/formats.go similarity index 98% rename from web/formats.go rename to app/formats.go index 007f6e1..1b15f85 100644 --- a/web/formats.go +++ b/app/formats.go @@ -1,4 +1,4 @@ -package web +package app import ( "github.com/samber/lo" diff --git a/app/router.go b/app/router.go new file mode 100644 index 0000000..91109ff --- /dev/null +++ b/app/router.go @@ -0,0 +1,166 @@ +package app + +import ( + "fmt" + "net/http" + "net/url" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "github.com/samber/lo" + "github.com/spf13/viper" + "go.fifitido.net/ytdl-web/pkg/httpx" + "go.fifitido.net/ytdl-web/pkg/view" + "go.fifitido.net/ytdl-web/version" + "go.fifitido.net/ytdl-web/ytdl" + "go.fifitido.net/ytdl-web/ytdl/metadata" + "golang.org/x/exp/slog" +) + +func Router(ytdl ytdl.Ytdl) http.Handler { + r := chi.NewRouter() + + r.Use(middleware.RequestID) + r.Use(middleware.RealIP) + r.Use(middleware.Logger) + r.Use(middleware.Recoverer) + + r.Get("/", indexHandler(ytdl)) + r.Get("/home", homeIndex) + + r.Route("/download", func(r chi.Router) { + r.Get("/", listDownloads(ytdl)) + r.Head("/proxy", proxyDownload(ytdl)) + r.Get("/proxy", proxyDownload(ytdl)) + }) + + return r +} + +func indexHandler(ytdl ytdl.Ytdl) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + views.Render(w, "index", view.Data{ + "BasePath": viper.GetString("base_path"), + "Version": version.Version, + "Build": version.Build, + "BinaryVersion": ytdl.Version(), + }, "layouts/main") + } +} + +func homeIndex(w http.ResponseWriter, r *http.Request) { + views.Render(w, "index", view.Data{ + "BasePath": viper.GetString("base_path"), + }) +} + +func listDownloads(ytdl ytdl.Ytdl) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + urlBytes, err := url.QueryUnescape(r.URL.Query()["url"][0]) + if err != nil { + views.Render(w, "index", view.Data{ + "BasePath": viper.GetString("base_path"), + "Error": view.Data{ + "Message": "Invalid URL", + }, + }) + return + } + url := string(urlBytes) + if len(url) < 1 { + views.Render(w, "index", view.Data{ + "BasePath": viper.GetString("base_path"), + "Error": view.Data{ + "Message": "Invalid URL", + }, + }) + return + } + + meta, err := ytdl.GetMetadata(url) + if err != nil { + views.Render(w, "index", view.Data{ + "BasePath": viper.GetString("base_path"), + "Error": view.Data{ + "Message": "Could not find a video at that url, maybe try again?", + }, + }) + return + } + + views.Render(w, "download", view.Data{ + "BasePath": viper.GetString("base_path"), + "Url": url, + "Meta": meta, + "Videos": GetVideos(meta), + }) + } +} + +func proxyDownload(ytdl ytdl.Ytdl) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + rawUrl, err := httpx.Query(r, "url") + if err != nil { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + urlBytes, err := url.QueryUnescape(rawUrl) + if err != nil { + slog.Error("Failed to decode url param", slog.String("error", err.Error())) + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + url := string(urlBytes) + if len(url) < 1 { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + formatId, err := httpx.Query(r, "format") + if err != nil { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + meta, err := ytdl.GetMetadata(url) + if err != nil { + slog.Error("Failed to get metadata", slog.String("error", err.Error())) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + videos := GetVideos(meta) + + index, err := httpx.QueryInt(r, "index") + if err != nil || index < 0 || index >= len(videos) { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + video := videos[index] + format, ok := lo.Find(video.Formats, func(format metadata.Format) bool { + return format.FormatID == formatId + }) + if !ok { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s-%s.%s\"", meta.ID, format.Resolution, format.Ext)) + if format.Filesize != nil { + w.Header().Set("Content-Length", fmt.Sprint(*format.Filesize)) + } else if format.FilesizeApprox != nil { + w.Header().Set("Content-Length", fmt.Sprint(*format.FilesizeApprox)) + } + + if len(videos) == 1 { + index = -1 + } + + if err := ytdl.Download(w, url, format.FormatID, index); err != nil { + slog.Error("Failed to download", slog.String("error", err.Error())) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + } +} diff --git a/web/serve.go b/app/serve.go similarity index 57% rename from web/serve.go rename to app/serve.go index 0b2b3c7..69a7bd8 100644 --- a/web/serve.go +++ b/app/serve.go @@ -1,11 +1,10 @@ -package web +package app import ( "fmt" + "net/http" "github.com/dgraph-io/badger/v2" - "github.com/gofiber/fiber/v2" - slogfiber "github.com/samber/slog-fiber" "go.fifitido.net/ytdl-web/config" "go.fifitido.net/ytdl-web/utils" "go.fifitido.net/ytdl-web/ytdl" @@ -14,16 +13,7 @@ import ( ) func Serve(cfg *config.Config) error { - engine := ViewsEngine() - app := fiber.New(fiber.Config{ - Views: engine, - EnableTrustedProxyCheck: true, - TrustedProxies: cfg.HTTP.TrustedProxies, - DisableStartupMessage: true, - }) - - logger := slog.With("module", "web") - app.Use(slogfiber.New(logger)) + logger := slog.Default() db, err := badger.Open( badger. @@ -36,15 +26,10 @@ func Serve(cfg *config.Config) error { defer db.Close() cache := cache.NewDefaultMetadataCache(db) - - routes := &routes{ - ytdl: ytdl.NewYtdl(cfg, slog.Default(), cache), - } - routes.Register(app) + ytdl := ytdl.NewYtdl(cfg, slog.Default(), cache) + router := Router(ytdl) listenAddr := fmt.Sprintf("%s:%d", cfg.HTTP.Listen, cfg.HTTP.Port) - logger.Info("Starting HTTP server", slog.String("host", cfg.HTTP.Listen), slog.Int("port", cfg.HTTP.Port)) - - return app.Listen(listenAddr) + return http.ListenAndServe(listenAddr, router) } diff --git a/app/views.go b/app/views.go new file mode 100644 index 0000000..58d6403 --- /dev/null +++ b/app/views.go @@ -0,0 +1,49 @@ +package app + +import ( + "embed" + "encoding/json" + "html/template" + "net/url" + + "github.com/htfy96/reformism" + "go.fifitido.net/ytdl-web/pkg/view/html" + "go.fifitido.net/ytdl-web/ytdl/metadata" +) + +//go:embed views/* +var viewsfs embed.FS + +var views = html.New( + viewsfs, + html.DefaultOptions(). + WithBaseDir("views"). + WithFunction("unsafe", func(s string) template.HTML { + return template.HTML(s) + }). + WithFunction("queryEscape", func(s string) string { + return url.QueryEscape(s) + }). + WithFunction("jsonMarshal", func(s any) (string, error) { + j, err := json.MarshalIndent(s, "", " ") + if err != nil { + return "", err + } + return string(j), nil + }). + WithFunction("downloadContext", func(meta metadata.Metadata, url, basePath string, format metadata.Format) map[string]any { + return map[string]any{ + "Meta": meta, + "Url": url, + "BasePath": basePath, + "Format": format, + } + }). + WithFunctions(reformism.FuncsHTML), +) + +func init() { + if err := views.Load(); err != nil { + panic(err) + } +} diff --git a/web/views/download.html b/app/views/download.html similarity index 100% rename from web/views/download.html rename to app/views/download.html diff --git a/web/views/index.html b/app/views/index.html similarity index 100% rename from web/views/index.html rename to app/views/index.html diff --git a/web/views/layouts/main.html b/app/views/layouts/main.html similarity index 94% rename from web/views/layouts/main.html rename to app/views/layouts/main.html index 5f5f598..a70170e 100644 --- a/web/views/layouts/main.html +++ b/app/views/layouts/main.html @@ -64,10 +64,10 @@
- {{template "views/partials/navbar" .}} -
{{embed}}
+ {{template "partials/navbar" .}} +
{{yield}}
- {{template "views/partials/footer" .}} + {{template "partials/footer" .}}