From 668b36ad04d546212fa67369f2d7dcbabe325146 Mon Sep 17 00:00:00 2001 From: Evan Fiordeliso Date: Mon, 10 Nov 2025 13:56:15 -0500 Subject: [PATCH] Switch to templ for views and simplify routing --- .envrc | 21 +- .gitignore | 23 +- app/controllers/download.go | 180 -------- app/controllers/home.go | 55 --- app/views.go | 69 --- app/views/download.html | 63 --- app/views/index.html | 52 --- app/views/layouts/main.html | 75 ---- app/views/partials/footer.html | 20 - app/views/partials/navbar.html | 5 - cmd/completion.go | 85 ---- cmd/root.go | 45 +- cmd/serve.go | 68 --- config.example.yaml | 7 +- config/config.go | 3 + devenv.lock | 109 ++--- devenv.nix | 113 +++++ devenv.yaml | 14 +- flake.lock | 757 --------------------------------- flake.nix | 56 --- go.mod | 62 +-- go.sum | 106 ++++- pkg/components/footer.templ | 29 ++ pkg/components/footer_templ.go | 84 ++++ pkg/components/navbar.templ | 9 + pkg/components/navbar_templ.go | 40 ++ pkg/components/paste.templ | 36 ++ pkg/components/paste_templ.go | 75 ++++ pkg/htmx/htmx.go | 120 ------ pkg/httpx/query.go | 29 -- pkg/middlewarex/slog.go | 37 -- {app => pkg}/models/video.go | 0 pkg/server/controller.go | 7 - pkg/server/options.go | 25 +- pkg/server/routes.go | 144 +++++++ pkg/server/server.go | 55 +-- pkg/serverctx/context.go | 25 ++ pkg/utils/badgerlogger.go | 3 +- pkg/utils/logwriter.go | 3 +- pkg/view/engine.go | 10 - pkg/view/html/engine.go | 125 ------ pkg/view/html/options.go | 54 --- pkg/views/downloads.templ | 122 ++++++ pkg/views/downloads_templ.go | 463 ++++++++++++++++++++ pkg/views/helpers.go | 12 + pkg/views/home.templ | 66 +++ pkg/views/home_templ.go | 141 ++++++ pkg/views/layout.templ | 52 +++ pkg/views/layout_templ.go | 88 ++++ pkg/views/video.go | 46 ++ pkg/ytdl/ytdl.go | 26 +- 51 files changed, 1842 insertions(+), 2072 deletions(-) delete mode 100644 app/controllers/download.go delete mode 100644 app/controllers/home.go delete mode 100644 app/views.go delete mode 100644 app/views/download.html delete mode 100644 app/views/index.html delete mode 100644 app/views/layouts/main.html delete mode 100644 app/views/partials/footer.html delete mode 100644 app/views/partials/navbar.html delete mode 100644 cmd/completion.go delete mode 100644 cmd/serve.go create mode 100644 devenv.nix delete mode 100644 flake.lock delete mode 100644 flake.nix create mode 100644 pkg/components/footer.templ create mode 100644 pkg/components/footer_templ.go create mode 100644 pkg/components/navbar.templ create mode 100644 pkg/components/navbar_templ.go create mode 100644 pkg/components/paste.templ create mode 100644 pkg/components/paste_templ.go delete mode 100644 pkg/htmx/htmx.go delete mode 100644 pkg/httpx/query.go delete mode 100644 pkg/middlewarex/slog.go rename {app => pkg}/models/video.go (100%) delete mode 100644 pkg/server/controller.go create mode 100644 pkg/server/routes.go create mode 100644 pkg/serverctx/context.go delete mode 100644 pkg/view/engine.go delete mode 100644 pkg/view/html/engine.go delete mode 100644 pkg/view/html/options.go create mode 100644 pkg/views/downloads.templ create mode 100644 pkg/views/downloads_templ.go create mode 100644 pkg/views/helpers.go create mode 100644 pkg/views/home.templ create mode 100644 pkg/views/home_templ.go create mode 100644 pkg/views/layout.templ create mode 100644 pkg/views/layout_templ.go create mode 100644 pkg/views/video.go diff --git a/.envrc b/.envrc index fdef198..cc5c18b 100644 --- a/.envrc +++ b/.envrc @@ -1,11 +1,12 @@ -if ! has nix_direnv_version || ! nix_direnv_version 2.2.1; then - source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.2.1/direnvrc" "sha256-zelF0vLbEl5uaqrfIzbgNzJWGmLzCmYAkInj/LNxvKs=" -fi +#!/usr/bin/env bash -nix_direnv_watch_file nix/devenv.nix -nix_direnv_watch_file devenv.lock -nix_direnv_watch_file devenv.yaml -if ! use flake . --impure -then - echo "devenv could not be built. The devenv environment was not loaded. Make the necessary changes to devenv.nix and hit enter to try again." >&2 -fi \ No newline at end of file +export DIRENV_WARN_TIMEOUT=20s + +eval "$(devenv direnvrc)" + +# `use devenv` supports the same options as the `devenv shell` command. +# +# To silence all output, use `--quiet`. +# +# Example usage: use devenv --quiet --impure --option services.postgres.enable:bool true +use devenv diff --git a/.gitignore b/.gitignore index d36b53e..9c4a351 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,25 @@ devenv.local.nix .direnv # pre-commit -.pre-commit-config.yaml \ No newline at end of file +.pre-commit-config.yaml +# Devenv +.devenv* +devenv.local.nix +devenv.local.yaml + +# direnv +.direnv + +# pre-commit +.pre-commit-config.yaml + +# Devenv +.devenv* +devenv.local.nix +devenv.local.yaml + +# direnv +.direnv + +# pre-commit +.pre-commit-config.yaml diff --git a/app/controllers/download.go b/app/controllers/download.go deleted file mode 100644 index 56d6a55..0000000 --- a/app/controllers/download.go +++ /dev/null @@ -1,180 +0,0 @@ -package controllers - -import ( - "fmt" - "io" - "net/http" - "net/url" - - "github.com/go-chi/chi/v5" - "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/pkg/ytdl" - "go.fifitido.net/ytdl-web/pkg/ytdl/metadata" - "go.fifitido.net/ytdl-web/version" - "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 urlBytes, true -} - -func (c *DownloadController) ListDownloadLinks(w http.ResponseWriter, r *http.Request) { - hx := htmx.New(w, r) - var layout []string - if !hx.IsHtmxRequest() { - layout = append(layout, "layouts/main") - } - - isSecure := r.URL.Scheme == "https" || r.Header.Get("X-Forwarded-Proto") == "https" - - videoUrl, ok := c.getUrlParam(r) - if !ok { - app.Views.Render(w, "index", view.Data{ - "BasePath": viper.GetString("base_path"), - "Version": version.Version, - "Build": version.Build, - "BinaryVersion": c.ytdl.Version(), - "IsSecure": isSecure, - "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"), - "Version": version.Version, - "Build": version.Build, - "BinaryVersion": c.ytdl.Version(), - "IsSecure": isSecure, - "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"), - "Version": version.Version, - "Build": version.Build, - "BinaryVersion": c.ytdl.Version(), - "IsSecure": isSecure, - "Url": videoUrl, - "Meta": meta, - "Videos": models.GetVideosFromMetadata(meta), - }, layout...) -} - -var ( - BUF_LEN = 1024 -) - -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] - - var format *metadata.Format - for _, f := range video.Formats { - if f.FormatID == formatId { - format = &f - break - } - } - if format == nil { - 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)) - } - - if len(videos) == 1 { - index = -1 - } - - read, write := io.Pipe() - - go func() { - _, err := io.Copy(w, read) - if err != nil { - slog.Error("Failed to copy", slog.String("error", err.Error())) - } - }() - - if err := c.ytdl.Download(write, 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) - } - - write.Close() -} diff --git a/app/controllers/home.go b/app/controllers/home.go deleted file mode 100644 index 5b7128c..0000000 --- a/app/controllers/home.go +++ /dev/null @@ -1,55 +0,0 @@ -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/pkg/ytdl" - "go.fifitido.net/ytdl-web/version" -) - -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) - isSecure := r.URL.Scheme == "https" || r.Header.Get("X-Forwarded-Proto") == "https" - - 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(), - "IsSecure": isSecure, - }) - } else { - app.Views.Render(w, "index", view.Data{ - "BasePath": viper.GetString("base_path"), - "Version": version.Version, - "Build": version.Build, - "BinaryVersion": c.ytdl.Version(), - "IsSecure": isSecure, - }, "layouts/main") - } -} diff --git a/app/views.go b/app/views.go deleted file mode 100644 index d779f67..0000000 --- a/app/views.go +++ /dev/null @@ -1,69 +0,0 @@ -package app - -import ( - "embed" - "encoding/json" - "fmt" - "html/template" - "net/url" - - "github.com/htfy96/reformism" - "go.fifitido.net/ytdl-web/pkg/view/html" - "go.fifitido.net/ytdl-web/pkg/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, - } - }). - WithFunction("sprintf", func(format string, args ...any) string { - return fmt.Sprintf(format, args...) - }). - WithFunction("filesize", func(size *int) string { - if size == nil { - return "unknown" - } - const unit = 1000 - if *size < unit { - return fmt.Sprintf("%d B", *size) - } - div, exp := int64(unit), 0 - for n := *size / unit; n >= unit; n /= unit { - div *= unit - exp++ - } - return fmt.Sprintf("%.1f %cB", - float64(*size)/float64(div), "kMGTPE"[exp]) - }). - WithFunctions(reformism.FuncsHTML), -) - -func init() { - if err := Views.Load(); err != nil { - panic(err) - } -} diff --git a/app/views/download.html b/app/views/download.html deleted file mode 100644 index 86ef67c..0000000 --- a/app/views/download.html +++ /dev/null @@ -1,63 +0,0 @@ -
-

Download Video

-

{{.Meta.Title}}

-

{{.Url}}

- - Download Another Video - -
- -{{$root := .}} -{{range $vidIndex, $video := .Videos}} -{{if not (eq $vidIndex 0)}} -
-{{end}} -
-
- {{$video.Meta.Title}} -
-
- {{range $index, $format := $video.Formats}} - {{template "format" (map "root" $root "format" $format "label" $format.Format "vidIndex" $vidIndex)}} - {{end}} -
-
-{{if gt (len $video.OtherFormats) 0}} -
- -
-
-
- {{range $index, $format := $video.OtherFormats}} - {{$label := sprintf "ext: %s, resolution: %s, filesize: %s, note: %s" $format.Ext $format.Resolution - (filesize $format.Filesize) $format.FormatNote}} - {{template "format" (map "root" $root "format" $format "label" $label "vidIndex" $vidIndex)}} - {{end}} -
-
-{{end}} -{{end}} - -{{define "format"}} -
{{.label}}
-
- - Download (direct) - - - Download (proxied) - -
-{{end}} \ No newline at end of file diff --git a/app/views/index.html b/app/views/index.html deleted file mode 100644 index 6e0f4fa..0000000 --- a/app/views/index.html +++ /dev/null @@ -1,52 +0,0 @@ -

YTDL Web

-

- Download videos from over a thousand websites with the help of - yt-dlp, a fork of youtube-dl - with more features and fixes. -
- View a complete list of supported websites - here. -

- -
-
- -
- - {{if .IsSecure}} - - {{end}} -
-
-
- -
-
- -{{if .Error}} - -{{end}} \ No newline at end of file diff --git a/app/views/layouts/main.html b/app/views/layouts/main.html deleted file mode 100644 index dcdb982..0000000 --- a/app/views/layouts/main.html +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - - YTDL Web - - - - - - - - - - -
- {{template "partials/navbar" .}} -
{{yield}}
-
- {{template "partials/footer" .}} - - - - \ No newline at end of file diff --git a/app/views/partials/footer.html b/app/views/partials/footer.html deleted file mode 100644 index 19aad37..0000000 --- a/app/views/partials/footer.html +++ /dev/null @@ -1,20 +0,0 @@ - \ No newline at end of file diff --git a/app/views/partials/navbar.html b/app/views/partials/navbar.html deleted file mode 100644 index d9cfd5e..0000000 --- a/app/views/partials/navbar.html +++ /dev/null @@ -1,5 +0,0 @@ - \ No newline at end of file diff --git a/cmd/completion.go b/cmd/completion.go deleted file mode 100644 index b4669b2..0000000 --- a/cmd/completion.go +++ /dev/null @@ -1,85 +0,0 @@ -/* -Copyright © 2024 NAME HERE -*/ -package cmd - -import ( - "fmt" - "os" - - "github.com/spf13/cobra" -) - -// completionCmd represents the completion command -var completionCmd = &cobra.Command{ - Use: "completion [bash|zsh|fish|powershell]", - Short: "Generate completion script", - Long: fmt.Sprintf(`To load completions: - -Bash: - - $ source <(%[1]s completion bash) - - # To load completions for each session, execute once: - # Linux: - $ %[1]s completion bash > /etc/bash_completion.d/%[1]s - # macOS: - $ %[1]s completion bash > $(brew --prefix)/etc/bash_completion.d/%[1]s - -Zsh: - - # If shell completion is not already enabled in your environment, - # you will need to enable it. You can execute the following once: - - $ echo "autoload -U compinit; compinit" >> ~/.zshrc - - # To load completions for each session, execute once: - $ %[1]s completion zsh > "${fpath[1]}/_%[1]s" - - # You will need to start a new shell for this setup to take effect. - -fish: - - $ %[1]s completion fish | source - - # To load completions for each session, execute once: - $ %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish - -PowerShell: - - PS> %[1]s completion powershell | Out-String | Invoke-Expression - - # To load completions for every new session, run: - PS> %[1]s completion powershell > %[1]s.ps1 - # and source this file from your PowerShell profile. -`, rootCmd.Root().Name()), - DisableFlagsInUseLine: true, - ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, - Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), - Run: func(cmd *cobra.Command, args []string) { - switch args[0] { - case "bash": - cmd.Root().GenBashCompletion(os.Stdout) - case "zsh": - cmd.Root().GenZshCompletion(os.Stdout) - case "fish": - cmd.Root().GenFishCompletion(os.Stdout, true) - case "powershell": - cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) - } - }, -} - -func init() { - rootCmd.AddCommand(completionCmd) - - // Here you will define your flags and configuration settings. - - // Cobra supports Persistent Flags which will work for this command - // and all subcommands, e.g.: - // completionCmd.PersistentFlags().String("foo", "", "A help for foo") - - // Cobra supports local flags which will only run when this command - // is called directly, e.g.: - // completionCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") -} diff --git a/cmd/root.go b/cmd/root.go index 7ca3bbf..55db51b 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -6,12 +6,17 @@ package cmd import ( "errors" "fmt" + "log/slog" "os" + "github.com/dgraph-io/badger/v2" "github.com/spf13/cobra" "github.com/spf13/viper" "go.fifitido.net/ytdl-web/config" - "golang.org/x/exp/slog" + "go.fifitido.net/ytdl-web/pkg/server" + "go.fifitido.net/ytdl-web/pkg/utils" + "go.fifitido.net/ytdl-web/pkg/ytdl" + "go.fifitido.net/ytdl-web/pkg/ytdl/cache" ) var ( @@ -23,6 +28,30 @@ var ( Short: "A web frontend for yt-dlp", Long: `YTDL Web is a web application that grabs the links to videos from over a thousand websites using the yt-dlp project under the hood.`, + RunE: func(cmd *cobra.Command, args []string) error { + initLogging() + logger := slog.Default() + + db, err := badger.Open( + badger. + DefaultOptions(cfg.Cache.DirPath). + WithLogger(utils.NewBadgerLogger(logger.With("module", "badger"))), + ) + if err != nil { + return err + } + defer db.Close() + + cache := cache.NewDefaultMetadataCache(db) + yt := ytdl.NewYtdl(cfg, slog.Default(), cache) + ytdl.SetDefault(yt) + + return server.ListenAndServe( + server.WithListenAddr(viper.GetString("http.listen")), + server.WithListenPort(viper.GetInt("http.port")), + server.WithLogger(logger.With("module", "server")), + ) + }, } ) @@ -42,14 +71,12 @@ func init() { rootCmd.PersistentFlags().BoolP("cookies-enabled", "C", false, "whether cookies are enabled") rootCmd.PersistentFlags().StringP("cookies", "c", "", "the path to the cookies file") - // trunk-ignore-begin(golangci-lint/errcheck): Ignoring errors viper.BindPFlag("http.port", rootCmd.PersistentFlags().Lookup("port")) viper.BindPFlag("http.listen", rootCmd.PersistentFlags().Lookup("listen")) viper.BindPFlag("http.basePath", rootCmd.PersistentFlags().Lookup("base-path")) viper.BindPFlag("ytdlp.binaryPath", rootCmd.PersistentFlags().Lookup("ytdlp-path")) viper.BindPFlag("cookies.enabled", rootCmd.PersistentFlags().Lookup("cookies-enabled")) viper.BindPFlag("cookies.filePath", rootCmd.PersistentFlags().Lookup("cookies")) - // trunk-ignore-end(golangci-lint/errcheck) } func initConfig() { @@ -73,19 +100,15 @@ func initConfig() { } func initLogging() { - var handler slog.Handler - if cfg.IsProduction() { - handler = slog.HandlerOptions{ + slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ AddSource: true, Level: slog.LevelInfo, - }.NewJSONHandler(os.Stdout) + }))) } else { - handler = slog.HandlerOptions{ + slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ AddSource: true, Level: slog.LevelDebug, - }.NewTextHandler(os.Stdout) + }))) } - - slog.SetDefault(slog.New(handler)) } diff --git a/cmd/serve.go b/cmd/serve.go deleted file mode 100644 index 0b3a634..0000000 --- a/cmd/serve.go +++ /dev/null @@ -1,68 +0,0 @@ -/* -Copyright © 2024 Evan Fiordeliso -*/ -package cmd - -import ( - "github.com/dgraph-io/badger/v2" - "github.com/spf13/cobra" - "github.com/spf13/viper" - "go.fifitido.net/ytdl-web/app/controllers" - "go.fifitido.net/ytdl-web/pkg/server" - "go.fifitido.net/ytdl-web/pkg/utils" - "go.fifitido.net/ytdl-web/pkg/ytdl" - "go.fifitido.net/ytdl-web/pkg/ytdl/cache" - "golang.org/x/exp/slog" -) - -var ( - serveCmd = &cobra.Command{ - Use: "serve", - Short: "Serve the ytdl-web application", - Long: `Serve the ytdl-web application`, - RunE: func(cmd *cobra.Command, args []string) error { - initLogging() - logger := slog.Default() - - db, err := badger.Open( - badger. - DefaultOptions(cfg.Cache.DirPath). - WithLogger(utils.NewBadgerLogger(logger.With("module", "badger"))), - ) - if err != nil { - return err - } - defer db.Close() - - cache := cache.NewDefaultMetadataCache(db) - ytdl := ytdl.NewYtdl(cfg, slog.Default(), cache) - - s := server.New( - server. - DefaultOptions(). - WithListenAddr(viper.GetString("http.listen")). - WithListenPort(viper.GetInt("http.port")). - WithLogger(logger.With("module", "server")), - ) - - s.MountController("/", controllers.NewHomeController(ytdl)) - s.MountController("/download", controllers.NewDownloadController(ytdl)) - - return s.ListenAndServe() - }, - } -) - -func init() { - rootCmd.AddCommand(serveCmd) - - // Here you will define your flags and configuration settings. - - // Cobra supports Persistent Flags which will work for this command - // and all subcommands, e.g.: - // completionCmd.PersistentFlags().String("foo", "", "A help for foo") - - // Cobra supports local flags which will only run when this command - // is called directly, e.g.: - // completionCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") -} diff --git a/config.example.yaml b/config.example.yaml index de92c02..fb9ff06 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -4,9 +4,10 @@ # For prod environments use Production # For staging envronments use Staging env: Production -# The path to the yt-dlp binary -# If it is already in your $PATH just yt-dlp will work. -binaryPath: yt-dlp +ytdlp: + # The path to the yt-dlp binary + # If it is already in your $PATH just yt-dlp will work. + binaryPath: yt-dlp http: # The port to listen on port: 8080 diff --git a/config/config.go b/config/config.go index a225eb4..5c4385a 100644 --- a/config/config.go +++ b/config/config.go @@ -1,6 +1,7 @@ package config import ( + "fmt" "os" "path" "strings" @@ -122,5 +123,7 @@ func LoadConfig(paths ...string) (*Config, error) { return nil, err } + fmt.Printf("%#v\n", config) + return config, nil } diff --git a/devenv.lock b/devenv.lock index 97c9f18..a6b5931 100644 --- a/devenv.lock +++ b/devenv.lock @@ -3,11 +3,10 @@ "devenv": { "locked": { "dir": "src/modules", - "lastModified": 1708141957, - "narHash": "sha256-IWkw+jsVpu7HFNPbOTJaQeMYQ5/eh7ZVScPvtlSo8vc=", + "lastModified": 1762791812, "owner": "cachix", "repo": "devenv", - "rev": "40b567388381137a3c49acdff5f4b6946d645a5f", + "rev": "1faab0d28c573f2a8dba2cf457b9d383adba252a", "type": "github" }, "original": { @@ -20,11 +19,10 @@ "flake-compat": { "flake": false, "locked": { - "lastModified": 1696426674, - "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "lastModified": 1761588595, "owner": "edolstra", "repo": "flake-compat", - "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5", "type": "github" }, "original": { @@ -33,37 +31,39 @@ "type": "github" } }, - "flake-utils": { + "git-hooks": { "inputs": { - "systems": "systems" + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": [ + "nixpkgs" + ] }, "locked": { - "lastModified": 1701680307, - "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", + "lastModified": 1762441963, + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "8e7576e79b88c16d7ee3bbd112c8d90070832885", "type": "github" }, "original": { - "owner": "numtide", - "repo": "flake-utils", + "owner": "cachix", + "repo": "git-hooks.nix", "type": "github" } }, "gitignore": { "inputs": { "nixpkgs": [ - "pre-commit-hooks", + "git-hooks", "nixpkgs" ] }, "locked": { - "lastModified": 1703887061, - "narHash": "sha256-gGPa9qWNc6eCXT/+Z5/zMkyYOuRZqeFZBDbopNZQkuY=", + "lastModified": 1709087332, "owner": "hercules-ci", "repo": "gitignore.nix", - "rev": "43e1aa1308018f37118e34d3a9cb4f5e75dc11d5", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", "type": "github" }, "original": { @@ -74,80 +74,27 @@ }, "nixpkgs": { "locked": { - "lastModified": 1708151420, - "narHash": "sha256-MGT/4aGCWQPQiu6COqJdCj9kSpLPiShgbwpbC38YXC8=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "6e2f00c83911461438301db0dba5281197fe4b3a", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs-stable": { - "locked": { - "lastModified": 1704874635, - "narHash": "sha256-YWuCrtsty5vVZvu+7BchAxmcYzTMfolSPP5io8+WYCg=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "3dc440faeee9e889fe2d1b4d25ad0f430d449356", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-23.11", - "repo": "nixpkgs", - "type": "github" - } - }, - "pre-commit-hooks": { - "inputs": { - "flake-compat": "flake-compat", - "flake-utils": "flake-utils", - "gitignore": "gitignore", - "nixpkgs": [ - "nixpkgs" - ], - "nixpkgs-stable": "nixpkgs-stable" - }, - "locked": { - "lastModified": 1708018599, - "narHash": "sha256-M+Ng6+SePmA8g06CmUZWi1AjG2tFBX9WCXElBHEKnyM=", + "lastModified": 1761313199, "owner": "cachix", - "repo": "pre-commit-hooks.nix", - "rev": "5df5a70ad7575f6601d91f0efec95dd9bc619431", + "repo": "devenv-nixpkgs", + "rev": "d1c30452ebecfc55185ae6d1c983c09da0c274ff", "type": "github" }, "original": { "owner": "cachix", - "repo": "pre-commit-hooks.nix", + "ref": "rolling", + "repo": "devenv-nixpkgs", "type": "github" } }, "root": { "inputs": { "devenv": "devenv", + "git-hooks": "git-hooks", "nixpkgs": "nixpkgs", - "pre-commit-hooks": "pre-commit-hooks" - } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" + "pre-commit-hooks": [ + "git-hooks" + ] } } }, diff --git a/devenv.nix b/devenv.nix new file mode 100644 index 0000000..c030849 --- /dev/null +++ b/devenv.nix @@ -0,0 +1,113 @@ +{ pkgs, lib, config, inputs, ... }: + +{ + # https://devenv.sh/basics/ + env = { + NAME = "ytdl-web"; + BINARY_OUT = "./out/ytdl-web"; + VERSION = "v1.2.3"; + VERSION_PKG = "go.fifitido.net/ytdl-web/version"; + DOCKER_REGISTRY = "git.fifitido.net"; + DOCKER_ORG = "apps"; + DOCKER_PLATFORMS = "linux/amd64,linux/arm64"; + + YTDL_ENV = "Development"; + }; + + # https://devenv.sh/packages/ + packages = with pkgs; [ + git + air + goreleaser + docker + docker-compose + buildkit + docker-buildx + yt-dlp + cobra-cli + ]; + + # https://devenv.sh/scripts/ + scripts = { + deps.exec = "go mod download"; + tidy.exec = "go mod tidy"; + check.exec = "goreleaser check"; + + build-id.exec = "git rev-parse --short HEAD"; + build-date.exec = "date -Iseconds"; + + build.exec = '' + go build -ldflags=" + -X $VERSION_PKG.Version=$VERSION + -X $VERSION_PKG.Build=$(build-id) + -X $VERSION_PKG.BuildDate=$(build-date) + -X $VERSION_PKG.BuiltBy=manual + " -o $BINARY_OUT . + ''; + clean.exec = '' + rm -rf ./dist/ ./out/ ./tmp/ + go clean + ''; + release.exec = '' + clean + goreleaser release + docker-build-release + ''; + lint.exec = "trunk check"; + fmt.exec = "trunk fmt"; + + docker-image.exec = "echo $DOCKER_REGISTRY/$DOCKER_ORG/$NAME"; + + docker-init.exec = '' + docker buildx create \ + --name $NAME \ + --platform $DOCKER_PLATFORMS + ''; + + docker-login.exec = "docker login $DOCKER_REGISTRY"; + + docker-build.exec = '' + platform=''${1:-linux} + output=''${2:-type=docker} + + docker-init + PROGRESS_NO_TRUNC=1 docker buildx build . \ + --tag $(docker-image):$VERSION \ + --tag $(docker-image):latest \ + --platform $platform \ + --builder $NAME \ + --build-arg VERSION=$VERSION \ + --build-arg BUILD=$(build-id) \ + --build-arg BUILD_DATE=$(build-date) \ + --build-arg BUILT_BY="Devenv Script" \ + --progress plain \ + --output $output + ''; + + docker-build-release.exec = "docker-build $DOCKER_PLATFORMS type=image,push=true"; + }; + + enterShell = '' + echo "Welcome to the $NAME development environment." + echo -n "Golang version: $(go version | cut -d ' ' -f 3), " + echo -n "Goreleaser version: $(goreleaser --version | tail -n 9 | head -n 1 | cut -c 16-), " + echo -n "Docker CLI version: $(docker version -f {{.Client.Version}}), " + echo -n "Buildx version: $(docker buildx version | cut -d ' ' -f 2), " + echo "yt-dlp version: $(yt-dlp --version)" + ''; + + # https://devenv.sh/languages/ + languages.go.enable = true; + + # https://devenv.sh/pre-commit-hooks/ + git-hooks.hooks = { + staticcheck.enable = true; + hadolint.enable = true; + yamllint.enable = true; + }; + + # https://devenv.sh/processes/ + processes.web.exec = "air"; + + # See full reference at https://devenv.sh/reference/options/ +} diff --git a/devenv.yaml b/devenv.yaml index b8609f7..72ad323 100644 --- a/devenv.yaml +++ b/devenv.yaml @@ -1,5 +1,15 @@ +# yaml-language-server: $schema=https://devenv.sh/devenv.schema.json --- -allowUnfree: true inputs: nixpkgs: - url: github:NixOS/nixpkgs/nixpkgs-unstable + url: github:cachix/devenv-nixpkgs/rolling +# If you're using non-OSS software, you can set allowUnfree to true. +# allowUnfree: true + +# If you're willing to use a package that's vulnerable +# permittedInsecurePackages: +# - "openssl-1.1.1w" + +# If you have more than one devenv you can merge them +# imports: +# - ./backend diff --git a/flake.lock b/flake.lock deleted file mode 100644 index 07264de..0000000 --- a/flake.lock +++ /dev/null @@ -1,757 +0,0 @@ -{ - "nodes": { - "cachix": { - "inputs": { - "devenv": "devenv_2", - "flake-compat": [ - "devenv", - "flake-compat" - ], - "git-hooks": [ - "devenv", - "pre-commit-hooks" - ], - "nixpkgs": [ - "devenv", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1726520618, - "narHash": "sha256-jOsaBmJ/EtX5t/vbylCdS7pWYcKGmWOKg4QKUzKr6dA=", - "owner": "cachix", - "repo": "cachix", - "rev": "695525f9086542dfb09fde0871dbf4174abbf634", - "type": "github" - }, - "original": { - "owner": "cachix", - "repo": "cachix", - "type": "github" - } - }, - "cachix_2": { - "inputs": { - "devenv": "devenv_3", - "flake-compat": [ - "devenv", - "cachix", - "devenv", - "flake-compat" - ], - "nixpkgs": [ - "devenv", - "cachix", - "devenv", - "nixpkgs" - ], - "pre-commit-hooks": [ - "devenv", - "cachix", - "devenv", - "pre-commit-hooks" - ] - }, - "locked": { - "lastModified": 1712055811, - "narHash": "sha256-7FcfMm5A/f02yyzuavJe06zLa9hcMHsagE28ADcmQvk=", - "owner": "cachix", - "repo": "cachix", - "rev": "02e38da89851ec7fec3356a5c04bc8349cae0e30", - "type": "github" - }, - "original": { - "owner": "cachix", - "repo": "cachix", - "type": "github" - } - }, - "devenv": { - "inputs": { - "cachix": "cachix", - "flake-compat": "flake-compat_2", - "nix": "nix_3", - "nixpkgs": "nixpkgs_3", - "pre-commit-hooks": "pre-commit-hooks_2" - }, - "locked": { - "lastModified": 1730890834, - "narHash": "sha256-ogmpmsPOlX4qeWVW4NZkTd0Lx8V4rvnjwlgOX7WnTZo=", - "owner": "cachix", - "repo": "devenv", - "rev": "c5353d1a0483b8f0dc15933de91c6b1b9a892831", - "type": "github" - }, - "original": { - "owner": "cachix", - "repo": "devenv", - "type": "github" - } - }, - "devenv_2": { - "inputs": { - "cachix": "cachix_2", - "flake-compat": [ - "devenv", - "cachix", - "flake-compat" - ], - "nix": "nix_2", - "nixpkgs": [ - "devenv", - "cachix", - "nixpkgs" - ], - "pre-commit-hooks": [ - "devenv", - "cachix", - "git-hooks" - ] - }, - "locked": { - "lastModified": 1723156315, - "narHash": "sha256-0JrfahRMJ37Rf1i0iOOn+8Z4CLvbcGNwa2ChOAVrp/8=", - "owner": "cachix", - "repo": "devenv", - "rev": "ff5eb4f2accbcda963af67f1a1159e3f6c7f5f91", - "type": "github" - }, - "original": { - "owner": "cachix", - "repo": "devenv", - "type": "github" - } - }, - "devenv_3": { - "inputs": { - "flake-compat": [ - "devenv", - "cachix", - "devenv", - "cachix", - "flake-compat" - ], - "nix": "nix", - "nixpkgs": "nixpkgs", - "poetry2nix": "poetry2nix", - "pre-commit-hooks": [ - "devenv", - "cachix", - "devenv", - "cachix", - "pre-commit-hooks" - ] - }, - "locked": { - "lastModified": 1708704632, - "narHash": "sha256-w+dOIW60FKMaHI1q5714CSibk99JfYxm0CzTinYWr+Q=", - "owner": "cachix", - "repo": "devenv", - "rev": "2ee4450b0f4b95a1b90f2eb5ffea98b90e48c196", - "type": "github" - }, - "original": { - "owner": "cachix", - "ref": "python-rewrite", - "repo": "devenv", - "type": "github" - } - }, - "flake-compat": { - "flake": false, - "locked": { - "lastModified": 1673956053, - "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", - "owner": "edolstra", - "repo": "flake-compat", - "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", - "type": "github" - }, - "original": { - "owner": "edolstra", - "repo": "flake-compat", - "type": "github" - } - }, - "flake-compat_2": { - "flake": false, - "locked": { - "lastModified": 1696426674, - "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", - "owner": "edolstra", - "repo": "flake-compat", - "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", - "type": "github" - }, - "original": { - "owner": "edolstra", - "repo": "flake-compat", - "type": "github" - } - }, - "flake-parts": { - "inputs": { - "nixpkgs-lib": [ - "devenv", - "nix", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1712014858, - "narHash": "sha256-sB4SWl2lX95bExY2gMFG5HIzvva5AVMJd4Igm+GpZNw=", - "owner": "hercules-ci", - "repo": "flake-parts", - "rev": "9126214d0a59633752a136528f5f3b9aa8565b7d", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "flake-parts", - "type": "github" - } - }, - "flake-parts_2": { - "inputs": { - "nixpkgs-lib": "nixpkgs-lib" - }, - "locked": { - "lastModified": 1730504689, - "narHash": "sha256-hgmguH29K2fvs9szpq2r3pz2/8cJd2LPS+b4tfNFCwE=", - "owner": "hercules-ci", - "repo": "flake-parts", - "rev": "506278e768c2a08bec68eb62932193e341f55c90", - "type": "github" - }, - "original": { - "id": "flake-parts", - "type": "indirect" - } - }, - "flake-utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1689068808, - "narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_2": { - "locked": { - "lastModified": 1667395993, - "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_3": { - "inputs": { - "systems": "systems_2" - }, - "locked": { - "lastModified": 1710146030, - "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "gitignore": { - "inputs": { - "nixpkgs": [ - "devenv", - "pre-commit-hooks", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1709087332, - "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", - "owner": "hercules-ci", - "repo": "gitignore.nix", - "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "gitignore.nix", - "type": "github" - } - }, - "libgit2": { - "flake": false, - "locked": { - "lastModified": 1697646580, - "narHash": "sha256-oX4Z3S9WtJlwvj0uH9HlYcWv+x1hqp8mhXl7HsLu2f0=", - "owner": "libgit2", - "repo": "libgit2", - "rev": "45fd9ed7ae1a9b74b957ef4f337bc3c8b3df01b5", - "type": "github" - }, - "original": { - "owner": "libgit2", - "repo": "libgit2", - "type": "github" - } - }, - "mk-shell-bin": { - "locked": { - "lastModified": 1677004959, - "narHash": "sha256-/uEkr1UkJrh11vD02aqufCxtbF5YnhRTIKlx5kyvf+I=", - "owner": "rrbutani", - "repo": "nix-mk-shell-bin", - "rev": "ff5d8bd4d68a347be5042e2f16caee391cd75887", - "type": "github" - }, - "original": { - "owner": "rrbutani", - "repo": "nix-mk-shell-bin", - "type": "github" - } - }, - "nix": { - "inputs": { - "flake-compat": "flake-compat", - "nixpkgs": [ - "devenv", - "cachix", - "devenv", - "cachix", - "devenv", - "nixpkgs" - ], - "nixpkgs-regression": "nixpkgs-regression" - }, - "locked": { - "lastModified": 1712911606, - "narHash": "sha256-BGvBhepCufsjcUkXnEEXhEVjwdJAwPglCC2+bInc794=", - "owner": "domenkozar", - "repo": "nix", - "rev": "b24a9318ea3f3600c1e24b4a00691ee912d4de12", - "type": "github" - }, - "original": { - "owner": "domenkozar", - "ref": "devenv-2.21", - "repo": "nix", - "type": "github" - } - }, - "nix-github-actions": { - "inputs": { - "nixpkgs": [ - "devenv", - "cachix", - "devenv", - "cachix", - "devenv", - "poetry2nix", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1688870561, - "narHash": "sha256-4UYkifnPEw1nAzqqPOTL2MvWtm3sNGw1UTYTalkTcGY=", - "owner": "nix-community", - "repo": "nix-github-actions", - "rev": "165b1650b753316aa7f1787f3005a8d2da0f5301", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "nix-github-actions", - "type": "github" - } - }, - "nix2container": { - "inputs": { - "flake-utils": "flake-utils_3", - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1730479402, - "narHash": "sha256-79NLeNjpCa4mSasmFsE3QA6obURezF0TUO5Pm+1daog=", - "owner": "nlewo", - "repo": "nix2container", - "rev": "5fb215a1564baa74ce04ad7f903d94ad6617e17a", - "type": "github" - }, - "original": { - "owner": "nlewo", - "repo": "nix2container", - "type": "github" - } - }, - "nix_2": { - "inputs": { - "flake-compat": [ - "devenv", - "cachix", - "devenv", - "flake-compat" - ], - "nixpkgs": [ - "devenv", - "cachix", - "devenv", - "nixpkgs" - ], - "nixpkgs-regression": "nixpkgs-regression_2" - }, - "locked": { - "lastModified": 1712911606, - "narHash": "sha256-BGvBhepCufsjcUkXnEEXhEVjwdJAwPglCC2+bInc794=", - "owner": "domenkozar", - "repo": "nix", - "rev": "b24a9318ea3f3600c1e24b4a00691ee912d4de12", - "type": "github" - }, - "original": { - "owner": "domenkozar", - "ref": "devenv-2.21", - "repo": "nix", - "type": "github" - } - }, - "nix_3": { - "inputs": { - "flake-compat": [ - "devenv", - "flake-compat" - ], - "flake-parts": "flake-parts", - "libgit2": "libgit2", - "nixpkgs": "nixpkgs_2", - "nixpkgs-23-11": "nixpkgs-23-11", - "nixpkgs-regression": "nixpkgs-regression_3", - "pre-commit-hooks": "pre-commit-hooks" - }, - "locked": { - "lastModified": 1727438425, - "narHash": "sha256-X8ES7I1cfNhR9oKp06F6ir4Np70WGZU5sfCOuNBEwMg=", - "owner": "domenkozar", - "repo": "nix", - "rev": "f6c5ae4c1b2e411e6b1e6a8181cc84363d6a7546", - "type": "github" - }, - "original": { - "owner": "domenkozar", - "ref": "devenv-2.24", - "repo": "nix", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1692808169, - "narHash": "sha256-x9Opq06rIiwdwGeK2Ykj69dNc2IvUH1fY55Wm7atwrE=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "9201b5ff357e781bf014d0330d18555695df7ba8", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs-23-11": { - "locked": { - "lastModified": 1717159533, - "narHash": "sha256-oamiKNfr2MS6yH64rUn99mIZjc45nGJlj9eGth/3Xuw=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "a62e6edd6d5e1fa0329b8653c801147986f8d446", - "type": "github" - }, - "original": { - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "a62e6edd6d5e1fa0329b8653c801147986f8d446", - "type": "github" - } - }, - "nixpkgs-lib": { - "locked": { - "lastModified": 1730504152, - "narHash": "sha256-lXvH/vOfb4aGYyvFmZK/HlsNsr/0CVWlwYvo2rxJk3s=", - "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs/archive/cc2f28000298e1269cea6612cd06ec9979dd5d7f.tar.gz" - }, - "original": { - "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs/archive/cc2f28000298e1269cea6612cd06ec9979dd5d7f.tar.gz" - } - }, - "nixpkgs-regression": { - "locked": { - "lastModified": 1643052045, - "narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2", - "type": "github" - }, - "original": { - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2", - "type": "github" - } - }, - "nixpkgs-regression_2": { - "locked": { - "lastModified": 1643052045, - "narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2", - "type": "github" - }, - "original": { - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2", - "type": "github" - } - }, - "nixpkgs-regression_3": { - "locked": { - "lastModified": 1643052045, - "narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2", - "type": "github" - }, - "original": { - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2", - "type": "github" - } - }, - "nixpkgs-stable": { - "locked": { - "lastModified": 1720386169, - "narHash": "sha256-NGKVY4PjzwAa4upkGtAMz1npHGoRzWotlSnVlqI40mo=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "194846768975b7ad2c4988bdb82572c00222c0d7", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-24.05", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_2": { - "locked": { - "lastModified": 1717432640, - "narHash": "sha256-+f9c4/ZX5MWDOuB1rKoWj+lBNm0z0rs4CK47HBLxy1o=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "88269ab3044128b7c2f4c7d68448b2fb50456870", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "release-24.05", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_3": { - "locked": { - "lastModified": 1716977621, - "narHash": "sha256-Q1UQzYcMJH4RscmpTkjlgqQDX5yi1tZL0O345Ri6vXQ=", - "owner": "cachix", - "repo": "devenv-nixpkgs", - "rev": "4267e705586473d3e5c8d50299e71503f16a6fb6", - "type": "github" - }, - "original": { - "owner": "cachix", - "ref": "rolling", - "repo": "devenv-nixpkgs", - "type": "github" - } - }, - "nixpkgs_4": { - "locked": { - "lastModified": 1716977621, - "narHash": "sha256-Q1UQzYcMJH4RscmpTkjlgqQDX5yi1tZL0O345Ri6vXQ=", - "owner": "cachix", - "repo": "devenv-nixpkgs", - "rev": "4267e705586473d3e5c8d50299e71503f16a6fb6", - "type": "github" - }, - "original": { - "owner": "cachix", - "ref": "rolling", - "repo": "devenv-nixpkgs", - "type": "github" - } - }, - "poetry2nix": { - "inputs": { - "flake-utils": "flake-utils", - "nix-github-actions": "nix-github-actions", - "nixpkgs": [ - "devenv", - "cachix", - "devenv", - "cachix", - "devenv", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1692876271, - "narHash": "sha256-IXfZEkI0Mal5y1jr6IRWMqK8GW2/f28xJenZIPQqkY0=", - "owner": "nix-community", - "repo": "poetry2nix", - "rev": "d5006be9c2c2417dafb2e2e5034d83fabd207ee3", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "poetry2nix", - "type": "github" - } - }, - "pre-commit-hooks": { - "inputs": { - "flake-compat": [ - "devenv", - "nix" - ], - "flake-utils": "flake-utils_2", - "gitignore": [ - "devenv", - "nix" - ], - "nixpkgs": [ - "devenv", - "nix", - "nixpkgs" - ], - "nixpkgs-stable": [ - "devenv", - "nix", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1712897695, - "narHash": "sha256-nMirxrGteNAl9sWiOhoN5tIHyjBbVi5e2tgZUgZlK3Y=", - "owner": "cachix", - "repo": "pre-commit-hooks.nix", - "rev": "40e6053ecb65fcbf12863338a6dcefb3f55f1bf8", - "type": "github" - }, - "original": { - "owner": "cachix", - "repo": "pre-commit-hooks.nix", - "type": "github" - } - }, - "pre-commit-hooks_2": { - "inputs": { - "flake-compat": [ - "devenv", - "flake-compat" - ], - "gitignore": "gitignore", - "nixpkgs": [ - "devenv", - "nixpkgs" - ], - "nixpkgs-stable": "nixpkgs-stable" - }, - "locked": { - "lastModified": 1726745158, - "narHash": "sha256-D5AegvGoEjt4rkKedmxlSEmC+nNLMBPWFxvmYnVLhjk=", - "owner": "cachix", - "repo": "pre-commit-hooks.nix", - "rev": "4e743a6920eab45e8ba0fbe49dc459f1423a4b74", - "type": "github" - }, - "original": { - "owner": "cachix", - "repo": "pre-commit-hooks.nix", - "type": "github" - } - }, - "root": { - "inputs": { - "devenv": "devenv", - "flake-parts": "flake-parts_2", - "mk-shell-bin": "mk-shell-bin", - "nix2container": "nix2container", - "nixpkgs": "nixpkgs_4" - } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_2": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/flake.nix b/flake.nix deleted file mode 100644 index 45c7c72..0000000 --- a/flake.nix +++ /dev/null @@ -1,56 +0,0 @@ -{ - description = "Description for the project"; - - inputs = { - nixpkgs.url = "github:cachix/devenv-nixpkgs/rolling"; - devenv.url = "github:cachix/devenv"; - nix2container.url = "github:nlewo/nix2container"; - nix2container.inputs.nixpkgs.follows = "nixpkgs"; - mk-shell-bin.url = "github:rrbutani/nix-mk-shell-bin"; - }; - - nixConfig = { - extra-trusted-public-keys = "devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw="; - extra-substituters = "https://devenv.cachix.org"; - }; - - outputs = inputs@{ self, flake-parts, ... }: - let - version = "1.2.0"; - rev = if (self ? rev) then self.rev else self.dirtyRev; - in - flake-parts.lib.mkFlake { inherit inputs; } { - imports = [ - inputs.devenv.flakeModule - ]; - systems = [ "x86_64-linux" "i686-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ]; - - perSystem = { config, self', inputs', pkgs, system, ... }: { - # Per-system attributes can be defined here. The self' and inputs' - # module parameters provide easy access to attributes of the same - # system. - - # needed for devenv up - packages.devenv-up = self'.devShells.default.config.procfileScript; - - packages.default = pkgs.callPackage ./nix/package.nix { inherit rev version; }; - - devenv.shells.default = { - name = "ytdl-web"; - - imports = [ - ./nix/devenv.nix - ]; - }; - - }; - - flake = { - # The usual flake attributes can be defined here, including system- - # agnostic ones like nixosModule and system-enumerating ones, although - # those are more easily expressed in perSystem. - - nixosModules.default = import ./nix/module.nix; - }; - }; -} diff --git a/go.mod b/go.mod index 8fcf5f0..ac0f230 100644 --- a/go.mod +++ b/go.mod @@ -1,44 +1,52 @@ module go.fifitido.net/ytdl-web -go 1.22 +go 1.25.2 require ( - github.com/adrg/xdg v0.4.0 + github.com/a-h/templ v0.3.960 + github.com/adrg/xdg v0.5.3 github.com/dgraph-io/badger/v2 v2.2007.4 - github.com/go-chi/chi/v5 v5.0.10 - github.com/htfy96/reformism v0.0.0-20160819020323-e5bfca398e73 - github.com/spf13/cobra v1.7.0 - github.com/spf13/viper v1.10.0 - golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 + github.com/go-chi/chi/v5 v5.2.3 + github.com/samber/slog-chi v1.17.0 + github.com/spf13/cobra v1.10.1 + github.com/spf13/viper v1.21.0 ) require ( github.com/cespare/xxhash v1.1.0 // indirect - github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de // indirect - github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 // indirect - github.com/dustin/go-humanize v1.0.0 // indirect - github.com/fsnotify/fsnotify v1.5.1 // indirect - github.com/golang/protobuf v1.5.2 // indirect - github.com/golang/snappy v0.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/dgraph-io/ristretto v0.2.0 // indirect + github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/golang/snappy v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/klauspost/compress v1.16.3 // indirect + github.com/klauspost/compress v1.18.1 // indirect github.com/kr/text v0.2.0 // indirect - github.com/magiconair/properties v1.8.5 // indirect - github.com/mitchellh/mapstructure v1.4.3 // indirect + github.com/magiconair/properties v1.8.10 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect - github.com/pelletier/go-toml v1.9.4 // indirect - github.com/pkg/errors v0.8.1 // indirect - github.com/spf13/afero v1.6.0 // indirect - github.com/spf13/cast v1.4.1 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/sagikazarmark/locafero v0.12.0 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect - github.com/subosito/gotenv v1.2.0 // indirect - golang.org/x/net v0.8.0 // indirect - golang.org/x/sys v0.7.0 // indirect - golang.org/x/text v0.8.0 // indirect - google.golang.org/protobuf v1.27.1 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.30.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect - gopkg.in/ini.v1 v1.66.2 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index d7734ad..0b66dad 100644 --- a/go.sum +++ b/go.sum @@ -1,16 +1,23 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/a-h/templ v0.3.960 h1:trshEpGa8clF5cdI39iY4ZrZG8Z/QixyzEyUnA7feTM= +github.com/a-h/templ v0.3.960/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo= github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= +github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= +github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -19,34 +26,49 @@ github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdw github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk= github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de h1:t0UHb5vdojIDUqktM6+xJAfScFBsVpXZmqC9dsgJmeA= github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= +github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE= +github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= +github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= -github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= -github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= -github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= +github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/htfy96/reformism v0.0.0-20160819020323-e5bfca398e73 h1:Shcv21tstWAyUkKxbn5bTARYej9sgEgFgTRxUPk1J8o= -github.com/htfy96/reformism v0.0.0-20160819020323-e5bfca398e73/go.mod h1:i2jduFeVras6pm8GnBWdfVKj97mGEXJogjMHzyJhukY= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY= github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= +github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -56,85 +78,133 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs= github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= +github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= +github.com/samber/slog-chi v1.17.0 h1:zP66fV4LGF1y1Dg/+uHNY9Uxkmw1YNWaZGws6M6mPCk= +github.com/samber/slog-chi v1.17.0/go.mod h1:a1iIuofF2gS1ii8aXIQhC6TEguLOhOvSM958fY5hToU= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.10.0 h1:mXH0UwHS4D2HwWZa75im4xIQynLfblmWV7qcWpfv0yk= github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 h1:5llv2sWeaMSnA3w2kS57ouQQ4pudlXrR0dCgw51QK9o= -golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/ini.v1 v1.66.2 h1:XfR1dOYubytKy4Shzc2LHrrGhU0lDCfDGG1yLPmpgsI= gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/pkg/components/footer.templ b/pkg/components/footer.templ new file mode 100644 index 0000000..4c1e8b5 --- /dev/null +++ b/pkg/components/footer.templ @@ -0,0 +1,29 @@ +package components + +import ( + "go.fifitido.net/ytdl-web/pkg/ytdl" + "go.fifitido.net/ytdl-web/version" +) + +templ Footer() { +
+
+
+
+
+ Version: + { version.Version } + (Build: { version.Build }) +
+
+ yt-dlp version: { ytdl.Default().Version() } +
+
+
+ Git Repository + © 2024 FiFiTiDo +
+
+
+
+} diff --git a/pkg/components/footer_templ.go b/pkg/components/footer_templ.go new file mode 100644 index 0000000..0a640d2 --- /dev/null +++ b/pkg/components/footer_templ.go @@ -0,0 +1,84 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.960 +package components + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import ( + "go.fifitido.net/ytdl-web/pkg/ytdl" + "go.fifitido.net/ytdl-web/version" +) + +func Footer() templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
Version: ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var2 string + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(version.Version) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/components/footer.templ`, Line: 15, Col: 48} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " (Build: ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(version.Build) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/components/footer.templ`, Line: 16, Col: 93} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, ")
yt-dlp version: ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(ytdl.Default().Version()) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/components/footer.templ`, Line: 19, Col: 73} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
Git Repository © 2024 FiFiTiDo
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/pkg/components/navbar.templ b/pkg/components/navbar.templ new file mode 100644 index 0000000..46c13e2 --- /dev/null +++ b/pkg/components/navbar.templ @@ -0,0 +1,9 @@ +package components + +templ Navbar() { + +} diff --git a/pkg/components/navbar_templ.go b/pkg/components/navbar_templ.go new file mode 100644 index 0000000..2044adc --- /dev/null +++ b/pkg/components/navbar_templ.go @@ -0,0 +1,40 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.960 +package components + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +func Navbar() templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/pkg/components/paste.templ b/pkg/components/paste.templ new file mode 100644 index 0000000..9b03658 --- /dev/null +++ b/pkg/components/paste.templ @@ -0,0 +1,36 @@ +package components + +import "go.fifitido.net/ytdl-web/pkg/serverctx" + +script pasteClipboard() { + const pasteButton = content.querySelector("#paste-button"); + const urlField = content.querySelector("#url"); + + try { + const text = await navigator.clipboard.readText(); + urlField.value = text; + } catch (error) { + toastr.error("Failed to paste url from clipboard."); + } +} + +templ PasteButton() { + if serverctx.IsHTTPS(ctx) { + + } +} diff --git a/pkg/components/paste_templ.go b/pkg/components/paste_templ.go new file mode 100644 index 0000000..a5259b3 --- /dev/null +++ b/pkg/components/paste_templ.go @@ -0,0 +1,75 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.960 +package components + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import "go.fifitido.net/ytdl-web/pkg/serverctx" + +func pasteClipboard() templ.ComponentScript { + return templ.ComponentScript{ + Name: `__templ_pasteClipboard_2c01`, + Function: `function __templ_pasteClipboard_2c01(){const pasteButton = content.querySelector("#paste-button"); + const urlField = content.querySelector("#url"); + + try { + const text = await navigator.clipboard.readText(); + urlField.value = text; + } catch (error) { + toastr.error("Failed to paste url from clipboard."); + } +}`, + Call: templ.SafeScript(`__templ_pasteClipboard_2c01`), + CallInline: templ.SafeScriptInline(`__templ_pasteClipboard_2c01`), + } +} + +func PasteButton() templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + if serverctx.IsHTTPS(ctx) { + templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, pasteClipboard()) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/pkg/htmx/htmx.go b/pkg/htmx/htmx.go deleted file mode 100644 index b242ab8..0000000 --- a/pkg/htmx/htmx.go +++ /dev/null @@ -1,120 +0,0 @@ -package htmx - -import "net/http" - -type HTMX struct { - w http.ResponseWriter - r *http.Request -} - -func New(w http.ResponseWriter, r *http.Request) *HTMX { - return &HTMX{ - w: w, - r: r, - } -} - -// -// Request header methods -// - -// True when the request is an HTMX request -func (h *HTMX) IsHtmxRequest() bool { - return h.r.Header.Get("HX-Request") == "true" -} - -// Indicates that the request is via an element using hx-boost -func (h *HTMX) IsBoosted() bool { - return h.r.Header.Get("HX-Boosted") == "true" -} - -// The current URL of the browser -func (h *HTMX) CurrentUrl() string { - return h.r.Header.Get("HX-Current-URL") -} - -// If the request is for history restoration after a miss in the local history cache -func (h *HTMX) IsHistoryRestore() bool { - return h.r.Header.Get("HX-History-Restore") == "true" -} - -// The id of the element that triggered the request -func (h *HTMX) TriggerId() string { - return h.r.Header.Get("HX-Trigger") -} - -// The name of the element that triggered the request -func (h *HTMX) TriggerName() string { - return h.r.Header.Get("HX-Trigger-Name") -} - -// The id of the target element -func (h *HTMX) TargetId() string { - return h.r.Header.Get("HX-Target") -} - -// The value entered by the user when prompted via hx-prompt -func (h *HTMX) Prompt() string { - return h.r.Header.Get("HX-Prompt") -} - -// -// Response header methods -// - -// Pushe a new url into the history stack -func (h *HTMX) PushUrl(url string) { - h.w.Header().Set("HX-Push", url) -} - -// Trigger a client-side redirect to a new location -func (h *HTMX) RedirectTo(url string) { - h.w.Header().Set("HX-Redirect", url) -} - -// Triggers a client-side redirect to a new location that acts as a swap -func (h *HTMX) Location(url string) { - h.w.Header().Set("Location", url) -} - -// Replace the current URL in the location bar -func (h *HTMX) ReplaceUrl(url string) { - h.w.Header().Set("HX-Replace-Url", url) -} - -// Sets refresh to true causing the client side to do a full refresh of the page -func (h *HTMX) Refresh() { - h.w.Header().Set("HX-Refresh", "true") -} - -// Trigger client side events -func (h *HTMX) Trigger(name string) { - h.w.Header().Set("HX-Trigger", name) -} - -// Trigger client side events after the swap step -func (h *HTMX) TriggerAfterSwap(name string) { - h.w.Header().Set("HX-Trigger-After-Swap", name) -} - -// Trigger client side events after the settle step -func (h *HTMX) TriggerAfterSettle(name string) { - h.w.Header().Set("HX-Trigger-After-Settle", name) -} - -// Specify how the response will be swapped. See hx-swap for possible values -func (h *HTMX) Reswap(value string) { - h.w.Header().Set("HX-Reswap", value) -} - -// Update the target of the content update to a different element on the page -// Value should be a CSS selector. -func (h *HTMX) Retarget(value string) { - h.w.Header().Set("HX-Retarget", value) -} - -// Choose which part of the response is used to be swapped in. Overrides an existing hx-select on the triggering element. -// Value should be a CSS selector. -func (h *HTMX) Reselect(value string) { - h.w.Header().Set("HX-Reselect", value) -} diff --git a/pkg/httpx/query.go b/pkg/httpx/query.go deleted file mode 100644 index 3f0511d..0000000 --- a/pkg/httpx/query.go +++ /dev/null @@ -1,29 +0,0 @@ -package httpx - -import ( - "errors" - "net/http" - "strconv" -) - -var ( - ErrQueryKeyNotFound = errors.New("query key not found") -) - -func Query(r *http.Request, key string) (string, error) { - values, ok := r.URL.Query()[key] - if !ok || len(values) == 0 { - return "", ErrQueryKeyNotFound - } - - return values[0], nil -} - -func QueryInt(r *http.Request, key string) (int, error) { - value, err := Query(r, key) - if err != nil { - return 0, err - } - - return strconv.Atoi(value) -} diff --git a/pkg/middlewarex/slog.go b/pkg/middlewarex/slog.go deleted file mode 100644 index 795c79e..0000000 --- a/pkg/middlewarex/slog.go +++ /dev/null @@ -1,37 +0,0 @@ -package middlewarex - -import ( - "net/http" - "time" - - "github.com/go-chi/chi/v5/middleware" - "golang.org/x/exp/slog" -) - -func SlogRequestLogger(logger *slog.Logger) func(next http.Handler) http.Handler { - if logger == nil { - return func(next http.Handler) http.Handler { return next } - } - - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor) - ti := time.Now() - - defer func() { - reqLogger := logger.With( - slog.String("proto", r.Proto), - slog.String("path", r.URL.Path), - slog.String("reqId", middleware.GetReqID(r.Context())), - slog.Duration("lat", time.Since(ti)), - slog.Int("status", ww.Status()), - slog.Int("size", ww.BytesWritten()), - ) - - reqLogger.Info("Served") - }() - - next.ServeHTTP(ww, r) - }) - } -} diff --git a/app/models/video.go b/pkg/models/video.go similarity index 100% rename from app/models/video.go rename to pkg/models/video.go diff --git a/pkg/server/controller.go b/pkg/server/controller.go deleted file mode 100644 index d1cb610..0000000 --- a/pkg/server/controller.go +++ /dev/null @@ -1,7 +0,0 @@ -package server - -import "github.com/go-chi/chi/v5" - -type Controller interface { - Router(r chi.Router) -} diff --git a/pkg/server/options.go b/pkg/server/options.go index 71fa427..5cb1c51 100644 --- a/pkg/server/options.go +++ b/pkg/server/options.go @@ -1,6 +1,6 @@ package server -import "golang.org/x/exp/slog" +import "log/slog" type Options struct { ListenAddr string @@ -16,17 +16,22 @@ func DefaultOptions() *Options { } } -func (o *Options) WithListenAddr(addr string) *Options { - o.ListenAddr = addr - return o +type Option func(*Options) + +func WithListenAddr(addr string) Option { + return func(o *Options) { + o.ListenAddr = addr + } } -func (o *Options) WithListenPort(port int) *Options { - o.ListenPort = port - return o +func WithListenPort(port int) Option { + return func(o *Options) { + o.ListenPort = port + } } -func (o *Options) WithLogger(logger *slog.Logger) *Options { - o.Logger = logger - return o +func WithLogger(logger *slog.Logger) Option { + return func(o *Options) { + o.Logger = logger + } } diff --git a/pkg/server/routes.go b/pkg/server/routes.go new file mode 100644 index 0000000..5b9318c --- /dev/null +++ b/pkg/server/routes.go @@ -0,0 +1,144 @@ +package server + +import ( + "fmt" + "io" + "log/slog" + "net/http" + "net/url" + "strconv" + + "github.com/a-h/templ" + "github.com/go-chi/chi/v5" + "go.fifitido.net/ytdl-web/pkg/models" + "go.fifitido.net/ytdl-web/pkg/views" + "go.fifitido.net/ytdl-web/pkg/ytdl" + "go.fifitido.net/ytdl-web/pkg/ytdl/metadata" +) + +func Routes(r chi.Router) { + r.Get("/", home) + r.Route("/download", func(r chi.Router) { + r.Get("/", download) + r.Head("/proxy", proxyDownload) + r.Get("/proxy", proxyDownload) + }) +} + +func renderPage(w http.ResponseWriter, r *http.Request, component templ.Component) { + isHtmx := r.Header.Get("HX-Request") == "true" + + if isHtmx { + if err := templ.RenderFragments(r.Context(), w, component, "main-content"); err != nil { + slog.ErrorContext(r.Context(), "failed to render page", slog.Any("error", err)) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } + return + } + + component.Render(r.Context(), w) +} + +func home(w http.ResponseWriter, r *http.Request) { + renderPage(w, r, views.Home(nil)) +} + +func getUrlParam(r *http.Request) (string, bool) { + urlRaw := r.URL.Query().Get("url") + if urlRaw == "" { + return "", false + } + + urlUnescaped, err := url.QueryUnescape(urlRaw) + if err != nil || len(urlUnescaped) < 1 { + return "", false + } + + return urlUnescaped, true +} + +func download(w http.ResponseWriter, r *http.Request) { + ytdl := ytdl.Default() + + videoUrl, ok := getUrlParam(r) + if !ok { + renderPage(w, r, views.Home(&views.Error{Message: "Invalid URL"})) + return + } + + meta, err := ytdl.GetMetadata(videoUrl) + if err != nil { + renderPage(w, r, views.Home(&views.Error{Message: "Could not find a video at that url", RetryUrl: &videoUrl})) + return + } + + renderPage(w, r, views.Downloads(&views.DownloadsViewModel{Url: videoUrl, Meta: meta})) +} + +func proxyDownload(w http.ResponseWriter, r *http.Request) { + ytdl := ytdl.Default() + videoUrl, ok := getUrlParam(r) + if !ok { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + formatId := r.URL.Query().Get("format") + if formatId == "" { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + meta, err := 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 := strconv.Atoi(r.URL.Query().Get("index")) + if err != nil || index < 0 || index >= len(videos) { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + video := videos[index] + + var format *metadata.Format + for _, f := range video.Formats { + if f.FormatID == formatId { + format = &f + break + } + } + if format == nil { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.%s\"", meta.ID, format.Ext)) + if format.Filesize != nil { + w.Header().Set("Content-Length", fmt.Sprint(*format.Filesize)) + } + + if len(videos) == 1 { + index = -1 + } + + read, write := io.Pipe() + + go func() { + _, err := io.Copy(w, read) + if err != nil { + slog.Error("Failed to copy", slog.String("error", err.Error())) + } + }() + + if err := ytdl.Download(write, 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) + } + + write.Close() +} diff --git a/pkg/server/server.go b/pkg/server/server.go index 857fa7a..f7bb343 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -2,53 +2,34 @@ package server import ( "fmt" + "log/slog" "net/http" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" - "go.fifitido.net/ytdl-web/pkg/middlewarex" - "golang.org/x/exp/slog" + slogchi "github.com/samber/slog-chi" + "go.fifitido.net/ytdl-web/pkg/serverctx" ) -type Server interface { - MountController(path string, controller Controller) - ListenAndServe() error -} - -type DefaultServer struct { - r chi.Router - opts *Options -} - -var _ Server = (*DefaultServer)(nil) - -func New(options ...*Options) *DefaultServer { - var opts *Options - if len(options) > 0 { - opts = options[0] - } else { - opts = DefaultOptions() +func ListenAndServe(options ...Option) error { + opts := DefaultOptions() + for _, opt := range options { + opt(opts) } r := chi.NewRouter() - r.Use(middleware.RequestID) - r.Use(middleware.RealIP) - r.Use(middlewarex.SlogRequestLogger(opts.Logger)) - r.Use(middleware.Recoverer) + r.Use( + serverctx.Middleware, + middleware.RequestID, + middleware.RealIP, + slogchi.New(opts.Logger), + middleware.Recoverer, + ) - return &DefaultServer{ - r: r, - opts: opts, - } -} + r.Group(Routes) -func (s *DefaultServer) MountController(path string, controller Controller) { - s.r.Route(path, controller.Router) -} - -func (s *DefaultServer) ListenAndServe() error { - listenAddr := fmt.Sprintf("%s:%d", s.opts.ListenAddr, s.opts.ListenPort) - s.opts.Logger.Info("Starting HTTP server", slog.String("addr", s.opts.ListenAddr), slog.Int("port", s.opts.ListenPort)) - return http.ListenAndServe(listenAddr, s.r) + listenAddr := fmt.Sprintf("%s:%d", opts.ListenAddr, opts.ListenPort) + opts.Logger.Info("Starting HTTP server", slog.String("addr", opts.ListenAddr), slog.Int("port", opts.ListenPort)) + return http.ListenAndServe(listenAddr, r) } diff --git a/pkg/serverctx/context.go b/pkg/serverctx/context.go new file mode 100644 index 0000000..902c743 --- /dev/null +++ b/pkg/serverctx/context.go @@ -0,0 +1,25 @@ +package serverctx + +import ( + "context" + "net/http" +) + +type contextKey string + +const ( + isHTTPS contextKey = "isHTTPS" +) + +func Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + isHttps := r.URL.Scheme == "https" || r.Header.Get("X-Forwarded-Proto") == "https" + ctx := context.WithValue(r.Context(), isHTTPS, isHttps) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func IsHTTPS(ctx context.Context) bool { + isHttps, ok := ctx.Value(isHTTPS).(bool) + return ok && isHttps +} diff --git a/pkg/utils/badgerlogger.go b/pkg/utils/badgerlogger.go index 9a5a9bb..ea3493e 100644 --- a/pkg/utils/badgerlogger.go +++ b/pkg/utils/badgerlogger.go @@ -3,8 +3,9 @@ package utils import ( "fmt" + "log/slog" + "github.com/dgraph-io/badger/v2" - "golang.org/x/exp/slog" ) type SlogLogger struct { diff --git a/pkg/utils/logwriter.go b/pkg/utils/logwriter.go index 0b98df8..14138e4 100644 --- a/pkg/utils/logwriter.go +++ b/pkg/utils/logwriter.go @@ -3,8 +3,7 @@ package utils import ( "context" "io" - - "golang.org/x/exp/slog" + "log/slog" ) type loggerWriter struct { diff --git a/pkg/view/engine.go b/pkg/view/engine.go deleted file mode 100644 index 3730b8e..0000000 --- a/pkg/view/engine.go +++ /dev/null @@ -1,10 +0,0 @@ -package view - -import "net/http" - -type Data map[string]interface{} - -type Engine interface { - Load() error - Render(w http.ResponseWriter, view string, data Data, layout ...string) -} diff --git a/pkg/view/html/engine.go b/pkg/view/html/engine.go deleted file mode 100644 index 9a754ad..0000000 --- a/pkg/view/html/engine.go +++ /dev/null @@ -1,125 +0,0 @@ -package html - -import ( - "embed" - "html/template" - "io/fs" - "net/http" - "path/filepath" - "strings" - "sync" - - "go.fifitido.net/ytdl-web/pkg/view" -) - -type Engine struct { - fs embed.FS - tpl *template.Template - opts *Options - - loadOnce sync.Once - mu sync.Mutex -} - -var _ view.Engine = (*Engine)(nil) - -func New(fs embed.FS, options ...*Options) *Engine { - var opts *Options - if len(options) > 0 && options[0] != nil { - opts = options[0] - } else { - opts = DefaultOptions() - } - - tpl := template.New("/") - tpl.Delims(opts.LeftDelim, opts.RightDelim) - tpl.Funcs(opts.FuncMap) - - return &Engine{ - fs: fs, - tpl: tpl, - opts: opts, - } -} - -func (e *Engine) Load() error { - var err error - e.loadOnce.Do(func() { - err = e.doLoad() - }) - return err -} - -func (e *Engine) doLoad() error { - e.mu.Lock() - defer e.mu.Unlock() - - return fs.WalkDir(e.fs, ".", func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - - if d == nil || d.IsDir() { - return nil - } - - if len(path) < len(e.opts.Extension) || path[len(path)-len(e.opts.Extension):] != e.opts.Extension { - return nil - } - - rel, err := filepath.Rel(e.opts.BaseDir, path) - if err != nil { - return err - } - - name := filepath.ToSlash(rel) - name = strings.TrimSuffix(name, e.opts.Extension) - - buf, err := e.fs.ReadFile(path) - if err != nil { - return err - } - - _, err = e.tpl.New(name).Parse(string(buf)) - if err != nil { - return err - } - - return nil - }) -} - -func (e *Engine) Render(w http.ResponseWriter, view string, data view.Data, layout ...string) { - tmpl := e.tpl.Lookup(view) - if tmpl == nil { - http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) - return - } - - if len(layout) > 0 && layout[0] != "" { - lay := e.tpl.Lookup(layout[0]) - if lay == nil { - http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) - return - } - - e.mu.Lock() - defer e.mu.Unlock() - - lay.Funcs(map[string]interface{}{ - "yield": func() error { - return tmpl.Execute(w, data) - }, - }) - - if err := lay.Execute(w, data); err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - } - return - } - - if err := tmpl.Execute(w, data); err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } -} diff --git a/pkg/view/html/options.go b/pkg/view/html/options.go deleted file mode 100644 index 99a035d..0000000 --- a/pkg/view/html/options.go +++ /dev/null @@ -1,54 +0,0 @@ -package html - -import ( - "fmt" - "html/template" -) - -type Options struct { - LeftDelim string - RightDelim string - Extension string - BaseDir string - FuncMap template.FuncMap -} - -func DefaultOptions() *Options { - return &Options{ - LeftDelim: "{{", - RightDelim: "}}", - Extension: ".html", - BaseDir: "/", - FuncMap: template.FuncMap{ - "yield": func() error { return fmt.Errorf("yield called outside of a layout") }, - }, - } -} - -func (o *Options) WithDelimiters(left, right string) *Options { - o.LeftDelim = left - o.RightDelim = right - return o -} - -func (o *Options) WithExtension(ext string) *Options { - o.Extension = ext - return o -} - -func (o *Options) WithBaseDir(dir string) *Options { - o.BaseDir = dir - return o -} - -func (o *Options) WithFunction(name string, fn interface{}) *Options { - o.FuncMap[name] = fn - return o -} - -func (o *Options) WithFunctions(funcs template.FuncMap) *Options { - for k, v := range funcs { - o.FuncMap[k] = v - } - return o -} diff --git a/pkg/views/downloads.templ b/pkg/views/downloads.templ new file mode 100644 index 0000000..2c5297e --- /dev/null +++ b/pkg/views/downloads.templ @@ -0,0 +1,122 @@ +package views + +import ( + "fmt" + "go.fifitido.net/ytdl-web/pkg/ytdl/metadata" +) + +type DownloadsViewModel struct { + Url string + Meta *metadata.Metadata +} + +templ videoFormatView(vm *DownloadsViewModel, vidIndex int, format metadata.Format, label string) { + {{ filename := fmt.Sprintf("%s-%s.%s", vm.Meta.ID, format.Resolution, format.Ext) }} +
{ label }
+ +} + +templ videoView(vm *DownloadsViewModel, index int, video *VideoViewModel) { + if index != 0 { +
+ } +
+ {{ + thumbnail := video.Meta.Thumbnail + thumbnailAlt := video.Meta.ID + if video.Meta.Title != nil { + thumbnailAlt = *video.Meta.Title + } + }} + if thumbnail != nil { +
+ { +
+ } +
+ for _, format := range video.Formats { + @videoFormatView(vm, index, format, format.Format) + } +
+
+ if len(video.OtherFormats) > 0 { + {{ collapseId := fmt.Sprintf("collapse-%s-%d", vm.Meta.ID, index) }} +
+ +
+
+
+ for _, format := range video.OtherFormats { + {{ label := fmt.Sprintf("ext: %s, resolution: %s, filesize: %d, note: %s", format.Ext, format.Resolution, format.Filesize, format.FormatNote) }} + @videoFormatView(vm, index, format, label) + } +
+
+ } +} + +css downloads() { + display: grid; + grid-template-columns: minmax(auto, max-content) auto; + gap: 1.5rem; + align-items: center; +} + +css fullWidth() { + grid-column: span 2; +} + +templ Downloads(vm *DownloadsViewModel) { + {{ videos := GetVideosFromMetadata(vm.Meta) }} + @Layout() { +
+

Download Video

+

{ *vm.Meta.Title }

+

{ vm.Url }

+ + Download Another Video + +
+ for index, video := range videos { + @videoView(vm, index, video) + } + } +} diff --git a/pkg/views/downloads_templ.go b/pkg/views/downloads_templ.go new file mode 100644 index 0000000..001f06a --- /dev/null +++ b/pkg/views/downloads_templ.go @@ -0,0 +1,463 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.960 +package views + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import ( + "fmt" + "go.fifitido.net/ytdl-web/pkg/ytdl/metadata" +) + +type DownloadsViewModel struct { + Url string + Meta *metadata.Metadata +} + +func videoFormatView(vm *DownloadsViewModel, vidIndex int, format metadata.Format, label string) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + filename := fmt.Sprintf("%s-%s.%s", vm.Meta.ID, format.Resolution, format.Ext) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var2 string + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(label) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/views/downloads.templ`, Line: 15, Col: 40} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func videoView(vm *DownloadsViewModel, index int, video *VideoViewModel) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var8 := templ.GetChildren(ctx) + if templ_7745c5c3_Var8 == nil { + templ_7745c5c3_Var8 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + if index != 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + thumbnail := video.Meta.Thumbnail + thumbnailAlt := video.Meta.ID + if video.Meta.Title != nil { + thumbnailAlt = *video.Meta.Title + } + if thumbnail != nil { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
\"")
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + var templ_7745c5c3_Var11 = []any{downloads(), "flex-lg-grow-1"} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var11...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, format := range video.Formats { + templ_7745c5c3_Err = videoFormatView(vm, index, format, format.Format).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if len(video.OtherFormats) > 0 { + collapseId := fmt.Sprintf("collapse-%s-%d", vm.Meta.ID, index) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var15 = []any{fullWidth(), "collapse"} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var15...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var18 = []any{downloads(), "d-flex d-md-grid flex-column"} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var18...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, format := range video.OtherFormats { + label := fmt.Sprintf("ext: %s, resolution: %s, filesize: %d, note: %s", format.Ext, format.Resolution, format.Filesize, format.FormatNote) + templ_7745c5c3_Err = videoFormatView(vm, index, format, label).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return nil + }) +} + +func downloads() templ.CSSClass { + templ_7745c5c3_CSSBuilder := templruntime.GetBuilder() + templ_7745c5c3_CSSBuilder.WriteString(`display:grid;`) + templ_7745c5c3_CSSBuilder.WriteString(`grid-template-columns:minmax(auto, max-content) auto;`) + templ_7745c5c3_CSSBuilder.WriteString(`gap:1.5rem;`) + templ_7745c5c3_CSSBuilder.WriteString(`align-items:center;`) + templ_7745c5c3_CSSID := templ.CSSID(`downloads`, templ_7745c5c3_CSSBuilder.String()) + return templ.ComponentCSSClass{ + ID: templ_7745c5c3_CSSID, + Class: templ.SafeCSS(`.` + templ_7745c5c3_CSSID + `{` + templ_7745c5c3_CSSBuilder.String() + `}`), + } +} + +func fullWidth() templ.CSSClass { + templ_7745c5c3_CSSBuilder := templruntime.GetBuilder() + templ_7745c5c3_CSSBuilder.WriteString(`grid-column:span 2;`) + templ_7745c5c3_CSSID := templ.CSSID(`fullWidth`, templ_7745c5c3_CSSBuilder.String()) + return templ.ComponentCSSClass{ + ID: templ_7745c5c3_CSSID, + Class: templ.SafeCSS(`.` + templ_7745c5c3_CSSID + `{` + templ_7745c5c3_CSSBuilder.String() + `}`), + } +} + +func Downloads(vm *DownloadsViewModel) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var20 := templ.GetChildren(ctx) + if templ_7745c5c3_Var20 == nil { + templ_7745c5c3_Var20 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + videos := GetVideosFromMetadata(vm.Meta) + templ_7745c5c3_Var21 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "

Download Video

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var22 string + templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(*vm.Meta.Title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/views/downloads.templ`, Line: 106, Col: 59} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var23 string + templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(vm.Url) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/views/downloads.templ`, Line: 107, Col: 41} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "

Download Another Video
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for index, video := range videos { + templ_7745c5c3_Err = videoView(vm, index, video).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return nil + }) + templ_7745c5c3_Err = Layout().Render(templ.WithChildren(ctx, templ_7745c5c3_Var21), templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/pkg/views/helpers.go b/pkg/views/helpers.go new file mode 100644 index 0000000..1d12666 --- /dev/null +++ b/pkg/views/helpers.go @@ -0,0 +1,12 @@ +package views + +import ( + "strings" + + "github.com/a-h/templ" + "github.com/spf13/viper" +) + +func pathTo(path ...string) templ.SafeURL { + return templ.SafeURL(viper.GetString("base_path") + "/" + strings.TrimPrefix(strings.Join(path, "/"), "/")) +} diff --git a/pkg/views/home.templ b/pkg/views/home.templ new file mode 100644 index 0000000..492714b --- /dev/null +++ b/pkg/views/home.templ @@ -0,0 +1,66 @@ +package views + +import "go.fifitido.net/ytdl-web/pkg/components" + +type Error struct { + Message string + RetryUrl *string +} + +templ Home(err *Error) { + @Layout() { +

YTDL Web

+

+ Download videos from over a thousand websites with the help of + yt-dlp, a fork of youtube-dl + with more features and fixes. +
+ View a complete list of supported websites + here. +

+
+
+ +
+ + @components.PasteButton() +
+
+
+ +
+
+ if err != nil { + + } + } +} diff --git a/pkg/views/home_templ.go b/pkg/views/home_templ.go new file mode 100644 index 0000000..6ea0697 --- /dev/null +++ b/pkg/views/home_templ.go @@ -0,0 +1,141 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.960 +package views + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import "go.fifitido.net/ytdl-web/pkg/components" + +type Error struct { + Message string + RetryUrl *string +} + +func Home(err *Error) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "

YTDL Web

Download videos from over a thousand websites with the help of yt-dlp, a fork of youtube-dl with more features and fixes.
View a complete list of supported websites here.

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = components.PasteButton().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if err != nil { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(err.Message) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/views/home.templ`, Line: 47, Col: 23} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if err.RetryUrl != nil { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return nil + }) + templ_7745c5c3_Err = Layout().Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/pkg/views/layout.templ b/pkg/views/layout.templ new file mode 100644 index 0000000..1691dc9 --- /dev/null +++ b/pkg/views/layout.templ @@ -0,0 +1,52 @@ +package views + +import "go.fifitido.net/ytdl-web/pkg/components" + +templ Layout() { + + + + + + + YTDL Web + + + + + + + + + +
+ @components.Navbar() + @templ.Fragment("main-content") { +
+ { children... } +
+ } +
+ @components.Footer() + + +} diff --git a/pkg/views/layout_templ.go b/pkg/views/layout_templ.go new file mode 100644 index 0000000..f068748 --- /dev/null +++ b/pkg/views/layout_templ.go @@ -0,0 +1,88 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.960 +package views + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import "go.fifitido.net/ytdl-web/pkg/components" + +func Layout() templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "YTDL Web
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = components.Navbar().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) + templ_7745c5c3_Err = templ.Fragment("main-content").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = components.Footer().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/pkg/views/video.go b/pkg/views/video.go new file mode 100644 index 0000000..0df815d --- /dev/null +++ b/pkg/views/video.go @@ -0,0 +1,46 @@ +package views + +import ( + "go.fifitido.net/ytdl-web/pkg/ytdl/metadata" +) + +type VideoViewModel struct { + Meta *metadata.Metadata + Formats []metadata.Format + OtherFormats []metadata.Format +} + +func GetVideosFromMetadata(meta *metadata.Metadata) []*VideoViewModel { + if meta.IsPlaylist() { + videos := make([]*VideoViewModel, 0, len(meta.Entries)) + + for _, entry := range meta.Entries { + videos = append(videos, GetVideosFromMetadata(&entry)...) + } + + return videos + } + + formats := []metadata.Format{} + otherFormats := []metadata.Format{} + + for _, format := range meta.Formats { + if format.ACodec != "none" && format.VCodec != "none" && format.Protocol != "m3u8_native" { + formats = append(formats, format) + } else { + otherFormats = append(otherFormats, format) + } + } + + for i, j := 0, len(formats)-1; i < j; i, j = i+1, j-1 { + formats[i], formats[j] = formats[j], formats[i] + } + + return []*VideoViewModel{ + { + Meta: meta, + Formats: formats, + OtherFormats: otherFormats, + }, + } +} diff --git a/pkg/ytdl/ytdl.go b/pkg/ytdl/ytdl.go index 03b8c47..150d809 100644 --- a/pkg/ytdl/ytdl.go +++ b/pkg/ytdl/ytdl.go @@ -3,15 +3,17 @@ package ytdl import ( "bytes" "encoding/json" + "errors" "fmt" "io" "os/exec" "strings" + "log/slog" + "go.fifitido.net/ytdl-web/config" "go.fifitido.net/ytdl-web/pkg/ytdl/cache" "go.fifitido.net/ytdl-web/pkg/ytdl/metadata" - "golang.org/x/exp/slog" ) type Ytdl interface { @@ -27,6 +29,16 @@ type ytdlImpl struct { version string } +var defaultYtdl Ytdl = &ytdlImpl{} + +func SetDefault(y Ytdl) { + defaultYtdl = y +} + +func Default() Ytdl { + return defaultYtdl +} + func NewYtdl(cfg *config.Config, logger *slog.Logger, cache cache.MetadataCache) Ytdl { cmd := exec.Command( cfg.Ytdlp.BinaryPath, @@ -102,13 +114,23 @@ func (y *ytdlImpl) GetMetadata(url string) (*metadata.Metadata, error) { args = y.appendCookieArgs(args) + fmt.Printf("ytdlp args: %#v\n", args) + cmd := exec.Command(y.cfg.Ytdlp.BinaryPath, args...) out, err := cmd.Output() if err != nil { attrs := []any{ slog.String("url", url), - slog.String("error", err.Error()), + } + + exiterr := &exec.ExitError{} + if errors.As(err, &exiterr) { + attrs = append(attrs, slog.Int("code", exiterr.ExitCode())) + attrs = append(attrs, slog.String("stderr", string(exiterr.Stderr))) + attrs = append(attrs, slog.String("error", exiterr.Error())) + } else { + attrs = append(attrs, slog.String("error", err.Error())) } y.logger.Error("failed to get metadata", attrs...)