diff --git a/app/controllers/download.go b/app/controllers/download.go new file mode 100644 index 0000000..1d132df --- /dev/null +++ b/app/controllers/download.go @@ -0,0 +1,148 @@ +package controllers + +import ( + "fmt" + "net/http" + "net/url" + + "github.com/go-chi/chi/v5" + "github.com/samber/lo" + "github.com/spf13/viper" + "go.fifitido.net/ytdl-web/app" + "go.fifitido.net/ytdl-web/app/models" + "go.fifitido.net/ytdl-web/pkg/htmx" + "go.fifitido.net/ytdl-web/pkg/httpx" + "go.fifitido.net/ytdl-web/pkg/server" + "go.fifitido.net/ytdl-web/pkg/view" + "go.fifitido.net/ytdl-web/ytdl" + "go.fifitido.net/ytdl-web/ytdl/metadata" + "golang.org/x/exp/slog" +) + +type DownloadController struct { + ytdl ytdl.Ytdl +} + +var _ server.Controller = (*DownloadController)(nil) + +func NewDownloadController(ytdl ytdl.Ytdl) *DownloadController { + return &DownloadController{ + ytdl: ytdl, + } +} + +func (c *DownloadController) Router(r chi.Router) { + r.Get("/", c.ListDownloadLinks) + r.Head("/proxy", c.ProxyDownload) + r.Get("/proxy", c.ProxyDownload) +} + +func (c *DownloadController) getUrlParam(r *http.Request) (string, bool) { + urlRaw, err := httpx.Query(r, "url") + if err != nil { + return "", false + } + + urlBytes, err := url.QueryUnescape(urlRaw) + if err != nil || len(urlBytes) < 1 { + return "", false + } + + return string(urlBytes), true +} + +func (c *DownloadController) ListDownloadLinks(w http.ResponseWriter, r *http.Request) { + hx := htmx.New(w, r) + layout := []string{} + if !hx.IsHtmxRequest() { + layout = append(layout, "layouts/main") + } + + videoUrl, ok := c.getUrlParam(r) + if !ok { + app.Views.Render(w, "index", view.Data{ + "BasePath": viper.GetString("base_path"), + "Error": view.Data{ + "Message": "Invalid URL", + }, + }, layout...) + return + } + + meta, err := c.ytdl.GetMetadata(videoUrl) + if err != nil { + app.Views.Render(w, "index", view.Data{ + "BasePath": viper.GetString("base_path"), + "Error": view.Data{ + "Message": "Could not find a video at that url", + "RetryUrl": videoUrl, + }, + }, layout...) + return + } + + if hx.IsHtmxRequest() { + hx.PushUrl("/download?url=" + url.QueryEscape(videoUrl)) + } + + app.Views.Render(w, "download", view.Data{ + "BasePath": viper.GetString("base_path"), + "Url": videoUrl, + "Meta": meta, + "Videos": models.GetVideosFromMetadata(meta), + }, layout...) +} + +func (c *DownloadController) ProxyDownload(w http.ResponseWriter, r *http.Request) { + videoUrl, ok := c.getUrlParam(r) + if !ok { + 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 := c.ytdl.GetMetadata(videoUrl) + 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 := models.GetVideosFromMetadata(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 := c.ytdl.Download(w, videoUrl, 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/app/controllers/home.go b/app/controllers/home.go new file mode 100644 index 0000000..90b3fca --- /dev/null +++ b/app/controllers/home.go @@ -0,0 +1,52 @@ +package controllers + +import ( + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/spf13/viper" + "go.fifitido.net/ytdl-web/app" + "go.fifitido.net/ytdl-web/pkg/htmx" + "go.fifitido.net/ytdl-web/pkg/server" + "go.fifitido.net/ytdl-web/pkg/view" + "go.fifitido.net/ytdl-web/version" + "go.fifitido.net/ytdl-web/ytdl" +) + +type HomeController struct { + ytdl ytdl.Ytdl +} + +var _ server.Controller = (*HomeController)(nil) + +func NewHomeController(ytdl ytdl.Ytdl) *HomeController { + return &HomeController{ + ytdl: ytdl, + } +} + +func (c *HomeController) Router(r chi.Router) { + r.Get("/", c.Index) +} + +func (c *HomeController) Index(w http.ResponseWriter, r *http.Request) { + hx := htmx.New(w, r) + + if hx.IsHtmxRequest() { + hx.PushUrl("/") + + app.Views.Render(w, "index", view.Data{ + "BasePath": viper.GetString("base_path"), + "Version": version.Version, + "Build": version.Build, + "BinaryVersion": c.ytdl.Version(), + }) + } else { + app.Views.Render(w, "index", view.Data{ + "BasePath": viper.GetString("base_path"), + "Version": version.Version, + "Build": version.Build, + "BinaryVersion": c.ytdl.Version(), + }, "layouts/main") + } +} diff --git a/app/formats.go b/app/models/video.go similarity index 83% rename from app/formats.go rename to app/models/video.go index 1b15f85..4231abe 100644 --- a/app/formats.go +++ b/app/models/video.go @@ -1,4 +1,4 @@ -package app +package models import ( "github.com/samber/lo" @@ -10,10 +10,10 @@ type Video struct { Formats []metadata.Format } -func GetVideos(meta *metadata.Metadata) []Video { +func GetVideosFromMetadata(meta *metadata.Metadata) []Video { if meta.IsPlaylist() { return lo.Map(meta.Entries, func(video metadata.Metadata, _ int) Video { - return GetVideos(&video)[0] + return GetVideosFromMetadata(&video)[0] }) } diff --git a/app/router.go b/app/router.go deleted file mode 100644 index 75c1807..0000000 --- a/app/router.go +++ /dev/null @@ -1,167 +0,0 @@ -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/middlewarex" - "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(middlewarex.SlogRequestLogger(slog.Default().With("module", "app"))) - 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/app/serve.go b/app/serve.go deleted file mode 100644 index 69a7bd8..0000000 --- a/app/serve.go +++ /dev/null @@ -1,35 +0,0 @@ -package app - -import ( - "fmt" - "net/http" - - "github.com/dgraph-io/badger/v2" - "go.fifitido.net/ytdl-web/config" - "go.fifitido.net/ytdl-web/utils" - "go.fifitido.net/ytdl-web/ytdl" - "go.fifitido.net/ytdl-web/ytdl/cache" - "golang.org/x/exp/slog" -) - -func Serve(cfg *config.Config) error { - logger := slog.Default() - - db, err := badger.Open( - badger. - DefaultOptions(cfg.Cache.DirPath). - WithLogger(utils.NewBadgerLogger(slog.With("module", "badger"))), - ) - if err != nil { - return err - } - defer db.Close() - - cache := cache.NewDefaultMetadataCache(db) - 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 http.ListenAndServe(listenAddr, router) -} diff --git a/app/views.go b/app/views.go index 58d6403..f0f0a87 100644 --- a/app/views.go +++ b/app/views.go @@ -14,7 +14,7 @@ import ( //go:embed views/* var viewsfs embed.FS -var views = html.New( +var Views = html.New( viewsfs, html.DefaultOptions(). WithBaseDir("views"). @@ -43,7 +43,7 @@ var views = html.New( ) func init() { - if err := views.Load(); err != nil { + if err := Views.Load(); err != nil { panic(err) } } diff --git a/app/views/download.html b/app/views/download.html index fc28ef7..4702586 100644 --- a/app/views/download.html +++ b/app/views/download.html @@ -2,15 +2,16 @@
{{.Url}}
- + {{$root := .}} {{range $vidIndex, $video := .Videos}} {{if not (eq $vidIndex diff --git a/app/views/index.html b/app/views/index.html index 60d8042..fc57eae 100644 --- a/app/views/index.html +++ b/app/views/index.html @@ -61,7 +61,29 @@ - {{if .Error}} -