Restructure to an MVC structure
This commit is contained in:
parent
3704bb2231
commit
b428fb2de2
|
@ -0,0 +1,148 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/viper"
|
||||
"go.fifitido.net/ytdl-web/app"
|
||||
"go.fifitido.net/ytdl-web/app/models"
|
||||
"go.fifitido.net/ytdl-web/pkg/htmx"
|
||||
"go.fifitido.net/ytdl-web/pkg/httpx"
|
||||
"go.fifitido.net/ytdl-web/pkg/server"
|
||||
"go.fifitido.net/ytdl-web/pkg/view"
|
||||
"go.fifitido.net/ytdl-web/ytdl"
|
||||
"go.fifitido.net/ytdl-web/ytdl/metadata"
|
||||
"golang.org/x/exp/slog"
|
||||
)
|
||||
|
||||
type DownloadController struct {
|
||||
ytdl ytdl.Ytdl
|
||||
}
|
||||
|
||||
var _ server.Controller = (*DownloadController)(nil)
|
||||
|
||||
func NewDownloadController(ytdl ytdl.Ytdl) *DownloadController {
|
||||
return &DownloadController{
|
||||
ytdl: ytdl,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *DownloadController) Router(r chi.Router) {
|
||||
r.Get("/", c.ListDownloadLinks)
|
||||
r.Head("/proxy", c.ProxyDownload)
|
||||
r.Get("/proxy", c.ProxyDownload)
|
||||
}
|
||||
|
||||
func (c *DownloadController) getUrlParam(r *http.Request) (string, bool) {
|
||||
urlRaw, err := httpx.Query(r, "url")
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
urlBytes, err := url.QueryUnescape(urlRaw)
|
||||
if err != nil || len(urlBytes) < 1 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return string(urlBytes), true
|
||||
}
|
||||
|
||||
func (c *DownloadController) ListDownloadLinks(w http.ResponseWriter, r *http.Request) {
|
||||
hx := htmx.New(w, r)
|
||||
layout := []string{}
|
||||
if !hx.IsHtmxRequest() {
|
||||
layout = append(layout, "layouts/main")
|
||||
}
|
||||
|
||||
videoUrl, ok := c.getUrlParam(r)
|
||||
if !ok {
|
||||
app.Views.Render(w, "index", view.Data{
|
||||
"BasePath": viper.GetString("base_path"),
|
||||
"Error": view.Data{
|
||||
"Message": "Invalid URL",
|
||||
},
|
||||
}, layout...)
|
||||
return
|
||||
}
|
||||
|
||||
meta, err := c.ytdl.GetMetadata(videoUrl)
|
||||
if err != nil {
|
||||
app.Views.Render(w, "index", view.Data{
|
||||
"BasePath": viper.GetString("base_path"),
|
||||
"Error": view.Data{
|
||||
"Message": "Could not find a video at that url",
|
||||
"RetryUrl": videoUrl,
|
||||
},
|
||||
}, layout...)
|
||||
return
|
||||
}
|
||||
|
||||
if hx.IsHtmxRequest() {
|
||||
hx.PushUrl("/download?url=" + url.QueryEscape(videoUrl))
|
||||
}
|
||||
|
||||
app.Views.Render(w, "download", view.Data{
|
||||
"BasePath": viper.GetString("base_path"),
|
||||
"Url": videoUrl,
|
||||
"Meta": meta,
|
||||
"Videos": models.GetVideosFromMetadata(meta),
|
||||
}, layout...)
|
||||
}
|
||||
|
||||
func (c *DownloadController) ProxyDownload(w http.ResponseWriter, r *http.Request) {
|
||||
videoUrl, ok := c.getUrlParam(r)
|
||||
if !ok {
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
formatId, err := httpx.Query(r, "format")
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
meta, err := c.ytdl.GetMetadata(videoUrl)
|
||||
if err != nil {
|
||||
slog.Error("Failed to get metadata", slog.String("error", err.Error()))
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
videos := models.GetVideosFromMetadata(meta)
|
||||
|
||||
index, err := httpx.QueryInt(r, "index")
|
||||
if err != nil || index < 0 || index >= len(videos) {
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
video := videos[index]
|
||||
format, ok := lo.Find(video.Formats, func(format metadata.Format) bool {
|
||||
return format.FormatID == formatId
|
||||
})
|
||||
if !ok {
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s-%s.%s\"", meta.ID, format.Resolution, format.Ext))
|
||||
if format.Filesize != nil {
|
||||
w.Header().Set("Content-Length", fmt.Sprint(*format.Filesize))
|
||||
} else if format.FilesizeApprox != nil {
|
||||
w.Header().Set("Content-Length", fmt.Sprint(*format.FilesizeApprox))
|
||||
}
|
||||
|
||||
if len(videos) == 1 {
|
||||
index = -1
|
||||
}
|
||||
|
||||
if err := c.ytdl.Download(w, videoUrl, format.FormatID, index); err != nil {
|
||||
slog.Error("Failed to download", slog.String("error", err.Error()))
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/spf13/viper"
|
||||
"go.fifitido.net/ytdl-web/app"
|
||||
"go.fifitido.net/ytdl-web/pkg/htmx"
|
||||
"go.fifitido.net/ytdl-web/pkg/server"
|
||||
"go.fifitido.net/ytdl-web/pkg/view"
|
||||
"go.fifitido.net/ytdl-web/version"
|
||||
"go.fifitido.net/ytdl-web/ytdl"
|
||||
)
|
||||
|
||||
type HomeController struct {
|
||||
ytdl ytdl.Ytdl
|
||||
}
|
||||
|
||||
var _ server.Controller = (*HomeController)(nil)
|
||||
|
||||
func NewHomeController(ytdl ytdl.Ytdl) *HomeController {
|
||||
return &HomeController{
|
||||
ytdl: ytdl,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *HomeController) Router(r chi.Router) {
|
||||
r.Get("/", c.Index)
|
||||
}
|
||||
|
||||
func (c *HomeController) Index(w http.ResponseWriter, r *http.Request) {
|
||||
hx := htmx.New(w, r)
|
||||
|
||||
if hx.IsHtmxRequest() {
|
||||
hx.PushUrl("/")
|
||||
|
||||
app.Views.Render(w, "index", view.Data{
|
||||
"BasePath": viper.GetString("base_path"),
|
||||
"Version": version.Version,
|
||||
"Build": version.Build,
|
||||
"BinaryVersion": c.ytdl.Version(),
|
||||
})
|
||||
} else {
|
||||
app.Views.Render(w, "index", view.Data{
|
||||
"BasePath": viper.GetString("base_path"),
|
||||
"Version": version.Version,
|
||||
"Build": version.Build,
|
||||
"BinaryVersion": c.ytdl.Version(),
|
||||
}, "layouts/main")
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package app
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/samber/lo"
|
||||
|
@ -10,10 +10,10 @@ type Video struct {
|
|||
Formats []metadata.Format
|
||||
}
|
||||
|
||||
func GetVideos(meta *metadata.Metadata) []Video {
|
||||
func GetVideosFromMetadata(meta *metadata.Metadata) []Video {
|
||||
if meta.IsPlaylist() {
|
||||
return lo.Map(meta.Entries, func(video metadata.Metadata, _ int) Video {
|
||||
return GetVideos(&video)[0]
|
||||
return GetVideosFromMetadata(&video)[0]
|
||||
})
|
||||
}
|
||||
|
167
app/router.go
167
app/router.go
|
@ -1,167 +0,0 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/viper"
|
||||
"go.fifitido.net/ytdl-web/pkg/httpx"
|
||||
"go.fifitido.net/ytdl-web/pkg/middlewarex"
|
||||
"go.fifitido.net/ytdl-web/pkg/view"
|
||||
"go.fifitido.net/ytdl-web/version"
|
||||
"go.fifitido.net/ytdl-web/ytdl"
|
||||
"go.fifitido.net/ytdl-web/ytdl/metadata"
|
||||
"golang.org/x/exp/slog"
|
||||
)
|
||||
|
||||
func Router(ytdl ytdl.Ytdl) http.Handler {
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Use(middleware.RequestID)
|
||||
r.Use(middleware.RealIP)
|
||||
r.Use(middlewarex.SlogRequestLogger(slog.Default().With("module", "app")))
|
||||
r.Use(middleware.Recoverer)
|
||||
|
||||
r.Get("/", indexHandler(ytdl))
|
||||
r.Get("/home", homeIndex)
|
||||
|
||||
r.Route("/download", func(r chi.Router) {
|
||||
r.Get("/", listDownloads(ytdl))
|
||||
r.Head("/proxy", proxyDownload(ytdl))
|
||||
r.Get("/proxy", proxyDownload(ytdl))
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func indexHandler(ytdl ytdl.Ytdl) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
views.Render(w, "index", view.Data{
|
||||
"BasePath": viper.GetString("base_path"),
|
||||
"Version": version.Version,
|
||||
"Build": version.Build,
|
||||
"BinaryVersion": ytdl.Version(),
|
||||
}, "layouts/main")
|
||||
}
|
||||
}
|
||||
|
||||
func homeIndex(w http.ResponseWriter, r *http.Request) {
|
||||
views.Render(w, "index", view.Data{
|
||||
"BasePath": viper.GetString("base_path"),
|
||||
})
|
||||
}
|
||||
|
||||
func listDownloads(ytdl ytdl.Ytdl) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
urlBytes, err := url.QueryUnescape(r.URL.Query()["url"][0])
|
||||
if err != nil {
|
||||
views.Render(w, "index", view.Data{
|
||||
"BasePath": viper.GetString("base_path"),
|
||||
"Error": view.Data{
|
||||
"Message": "Invalid URL",
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
url := string(urlBytes)
|
||||
if len(url) < 1 {
|
||||
views.Render(w, "index", view.Data{
|
||||
"BasePath": viper.GetString("base_path"),
|
||||
"Error": view.Data{
|
||||
"Message": "Invalid URL",
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
meta, err := ytdl.GetMetadata(url)
|
||||
if err != nil {
|
||||
views.Render(w, "index", view.Data{
|
||||
"BasePath": viper.GetString("base_path"),
|
||||
"Error": view.Data{
|
||||
"Message": "Could not find a video at that url, maybe try again?",
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
views.Render(w, "download", view.Data{
|
||||
"BasePath": viper.GetString("base_path"),
|
||||
"Url": url,
|
||||
"Meta": meta,
|
||||
"Videos": GetVideos(meta),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func proxyDownload(ytdl ytdl.Ytdl) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
rawUrl, err := httpx.Query(r, "url")
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
urlBytes, err := url.QueryUnescape(rawUrl)
|
||||
if err != nil {
|
||||
slog.Error("Failed to decode url param", slog.String("error", err.Error()))
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
url := string(urlBytes)
|
||||
if len(url) < 1 {
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
formatId, err := httpx.Query(r, "format")
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
meta, err := ytdl.GetMetadata(url)
|
||||
if err != nil {
|
||||
slog.Error("Failed to get metadata", slog.String("error", err.Error()))
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
videos := GetVideos(meta)
|
||||
|
||||
index, err := httpx.QueryInt(r, "index")
|
||||
if err != nil || index < 0 || index >= len(videos) {
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
video := videos[index]
|
||||
format, ok := lo.Find(video.Formats, func(format metadata.Format) bool {
|
||||
return format.FormatID == formatId
|
||||
})
|
||||
if !ok {
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s-%s.%s\"", meta.ID, format.Resolution, format.Ext))
|
||||
if format.Filesize != nil {
|
||||
w.Header().Set("Content-Length", fmt.Sprint(*format.Filesize))
|
||||
} else if format.FilesizeApprox != nil {
|
||||
w.Header().Set("Content-Length", fmt.Sprint(*format.FilesizeApprox))
|
||||
}
|
||||
|
||||
if len(videos) == 1 {
|
||||
index = -1
|
||||
}
|
||||
|
||||
if err := ytdl.Download(w, url, format.FormatID, index); err != nil {
|
||||
slog.Error("Failed to download", slog.String("error", err.Error()))
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
35
app/serve.go
35
app/serve.go
|
@ -1,35 +0,0 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/dgraph-io/badger/v2"
|
||||
"go.fifitido.net/ytdl-web/config"
|
||||
"go.fifitido.net/ytdl-web/utils"
|
||||
"go.fifitido.net/ytdl-web/ytdl"
|
||||
"go.fifitido.net/ytdl-web/ytdl/cache"
|
||||
"golang.org/x/exp/slog"
|
||||
)
|
||||
|
||||
func Serve(cfg *config.Config) error {
|
||||
logger := slog.Default()
|
||||
|
||||
db, err := badger.Open(
|
||||
badger.
|
||||
DefaultOptions(cfg.Cache.DirPath).
|
||||
WithLogger(utils.NewBadgerLogger(slog.With("module", "badger"))),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
cache := cache.NewDefaultMetadataCache(db)
|
||||
ytdl := ytdl.NewYtdl(cfg, slog.Default(), cache)
|
||||
router := Router(ytdl)
|
||||
|
||||
listenAddr := fmt.Sprintf("%s:%d", cfg.HTTP.Listen, cfg.HTTP.Port)
|
||||
logger.Info("Starting HTTP server", slog.String("host", cfg.HTTP.Listen), slog.Int("port", cfg.HTTP.Port))
|
||||
return http.ListenAndServe(listenAddr, router)
|
||||
}
|
|
@ -14,7 +14,7 @@ import (
|
|||
//go:embed views/*
|
||||
var viewsfs embed.FS
|
||||
|
||||
var views = html.New(
|
||||
var Views = html.New(
|
||||
viewsfs,
|
||||
html.DefaultOptions().
|
||||
WithBaseDir("views").
|
||||
|
@ -43,7 +43,7 @@ var views = html.New(
|
|||
)
|
||||
|
||||
func init() {
|
||||
if err := views.Load(); err != nil {
|
||||
if err := Views.Load(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,15 +2,16 @@
|
|||
<h1>Download Video</h1>
|
||||
<h2 class="fs-4 text-muted text-center">{{.Meta.Title}}</h2>
|
||||
<p style="font-size: 0.85rem">{{.Url}}</p>
|
||||
<button
|
||||
hx-get="{{.BasePath}}/home"
|
||||
<a
|
||||
href="{{.BasePath}}/"
|
||||
hx-get="{{.BasePath}}/"
|
||||
hx-trigger="click"
|
||||
hx-target="#main-content"
|
||||
class="btn btn-secondary btn-sm mt-3"
|
||||
style="width: 30rem; max-width: 100%"
|
||||
>
|
||||
Download Another Video
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{{$root := .}} {{range $vidIndex, $video := .Videos}} {{if not (eq $vidIndex
|
||||
|
|
|
@ -61,7 +61,29 @@
|
|||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{{if .Error}}
|
||||
<div class="alert alert-danger mt-4" role="" alert>{{.Error.Message}}</div>
|
||||
{{end}}
|
||||
</form>
|
||||
|
||||
{{if .Error}}
|
||||
<div class="alert alert-danger mt-4" role="alert">
|
||||
<span>{{.Error.Message}}</span>
|
||||
|
||||
{{if .Error.RetryUrl}}
|
||||
<button
|
||||
class="btn btn-link btn-sm pt-0 lh-base text-decoration-none"
|
||||
hx-get="/download"
|
||||
hx-trigger="click"
|
||||
hx-target="#main-content"
|
||||
hx-swap="innerHTML"
|
||||
hx-vals='{"url": "{{.Error.RetryUrl}}"}'
|
||||
>
|
||||
<span class="text-decoration-underline">Try Again</span>
|
||||
<div
|
||||
class="spinner-border spinner-border-sm htmx-indicator ms-1"
|
||||
role="status"
|
||||
>
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</button>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
38
cmd/root.go
38
cmd/root.go
|
@ -6,10 +6,15 @@ package cmd
|
|||
import (
|
||||
"os"
|
||||
|
||||
"github.com/dgraph-io/badger/v2"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"go.fifitido.net/ytdl-web/app"
|
||||
"go.fifitido.net/ytdl-web/app/controllers"
|
||||
"go.fifitido.net/ytdl-web/config"
|
||||
"go.fifitido.net/ytdl-web/pkg/server"
|
||||
"go.fifitido.net/ytdl-web/utils"
|
||||
"go.fifitido.net/ytdl-web/ytdl"
|
||||
"go.fifitido.net/ytdl-web/ytdl/cache"
|
||||
"golang.org/x/exp/slog"
|
||||
)
|
||||
|
||||
|
@ -24,10 +29,33 @@ var (
|
|||
|
||||
A web application that grabs the links to videos from over a
|
||||
thousand websites using the yt-dlp project under the hood.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := app.Serve(cfg); err != nil {
|
||||
slog.Error("Error when serving website", slog.String("error", err.Error()))
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
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.DefaultServerOptions().
|
||||
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()
|
||||
},
|
||||
}
|
||||
)
|
||||
|
@ -44,7 +72,7 @@ func init() {
|
|||
|
||||
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $XDG_CONFIG_HOME/ytdl-web/config.yml)")
|
||||
|
||||
rootCmd.PersistentFlags().IntP("port", "p", 0, "port to listen on")
|
||||
rootCmd.PersistentFlags().IntP("port", "p", 8080, "port to listen on")
|
||||
rootCmd.PersistentFlags().StringP("listen", "l", "", "address to listen on")
|
||||
rootCmd.PersistentFlags().StringP("base-path", "b", "", "the base path, used when behind reverse proxy")
|
||||
rootCmd.PersistentFlags().StringP("ytdlp-path", "y", "", "the path to the yt-dlp binary, used when it is not in $PATH")
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
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)
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package server
|
||||
|
||||
import "github.com/go-chi/chi/v5"
|
||||
|
||||
type Controller interface {
|
||||
Router(r chi.Router)
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package server
|
||||
|
||||
import "golang.org/x/exp/slog"
|
||||
|
||||
type ServerOptions struct {
|
||||
ListenAddr string
|
||||
ListenPort int
|
||||
Logger *slog.Logger
|
||||
}
|
||||
|
||||
func DefaultServerOptions() *ServerOptions {
|
||||
return &ServerOptions{
|
||||
ListenAddr: "127.0.0.1",
|
||||
ListenPort: 8080,
|
||||
Logger: slog.Default(),
|
||||
}
|
||||
}
|
||||
|
||||
func (o *ServerOptions) WithListenAddr(addr string) *ServerOptions {
|
||||
o.ListenAddr = addr
|
||||
return o
|
||||
}
|
||||
|
||||
func (o *ServerOptions) WithListenPort(port int) *ServerOptions {
|
||||
o.ListenPort = port
|
||||
return o
|
||||
}
|
||||
|
||||
func (o *ServerOptions) WithLogger(logger *slog.Logger) *ServerOptions {
|
||||
o.Logger = logger
|
||||
return o
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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"
|
||||
)
|
||||
|
||||
type Server interface {
|
||||
MountController(path string, controller Controller)
|
||||
ListenAndServe() error
|
||||
}
|
||||
|
||||
type DefaultServer struct {
|
||||
r chi.Router
|
||||
opts *ServerOptions
|
||||
}
|
||||
|
||||
var _ Server = (*DefaultServer)(nil)
|
||||
|
||||
func New(options ...*ServerOptions) *DefaultServer {
|
||||
var opts *ServerOptions
|
||||
if len(options) > 0 {
|
||||
opts = options[0]
|
||||
} else {
|
||||
opts = DefaultServerOptions()
|
||||
}
|
||||
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Use(middleware.RequestID)
|
||||
r.Use(middleware.RealIP)
|
||||
r.Use(middlewarex.SlogRequestLogger(opts.Logger))
|
||||
r.Use(middleware.Recoverer)
|
||||
|
||||
return &DefaultServer{
|
||||
r: r,
|
||||
opts: opts,
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
Loading…
Reference in New Issue