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 (
|
import (
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
|
@ -10,10 +10,10 @@ type Video struct {
|
||||||
Formats []metadata.Format
|
Formats []metadata.Format
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetVideos(meta *metadata.Metadata) []Video {
|
func GetVideosFromMetadata(meta *metadata.Metadata) []Video {
|
||||||
if meta.IsPlaylist() {
|
if meta.IsPlaylist() {
|
||||||
return lo.Map(meta.Entries, func(video metadata.Metadata, _ int) Video {
|
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/*
|
//go:embed views/*
|
||||||
var viewsfs embed.FS
|
var viewsfs embed.FS
|
||||||
|
|
||||||
var views = html.New(
|
var Views = html.New(
|
||||||
viewsfs,
|
viewsfs,
|
||||||
html.DefaultOptions().
|
html.DefaultOptions().
|
||||||
WithBaseDir("views").
|
WithBaseDir("views").
|
||||||
|
@ -43,7 +43,7 @@ var views = html.New(
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
if err := views.Load(); err != nil {
|
if err := Views.Load(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,15 +2,16 @@
|
||||||
<h1>Download Video</h1>
|
<h1>Download Video</h1>
|
||||||
<h2 class="fs-4 text-muted text-center">{{.Meta.Title}}</h2>
|
<h2 class="fs-4 text-muted text-center">{{.Meta.Title}}</h2>
|
||||||
<p style="font-size: 0.85rem">{{.Url}}</p>
|
<p style="font-size: 0.85rem">{{.Url}}</p>
|
||||||
<button
|
<a
|
||||||
hx-get="{{.BasePath}}/home"
|
href="{{.BasePath}}/"
|
||||||
|
hx-get="{{.BasePath}}/"
|
||||||
hx-trigger="click"
|
hx-trigger="click"
|
||||||
hx-target="#main-content"
|
hx-target="#main-content"
|
||||||
class="btn btn-secondary btn-sm mt-3"
|
class="btn btn-secondary btn-sm mt-3"
|
||||||
style="width: 30rem; max-width: 100%"
|
style="width: 30rem; max-width: 100%"
|
||||||
>
|
>
|
||||||
Download Another Video
|
Download Another Video
|
||||||
</button>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{$root := .}} {{range $vidIndex, $video := .Videos}} {{if not (eq $vidIndex
|
{{$root := .}} {{range $vidIndex, $video := .Videos}} {{if not (eq $vidIndex
|
||||||
|
|
|
@ -61,7 +61,29 @@
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{{if .Error}}
|
|
||||||
<div class="alert alert-danger mt-4" role="" alert>{{.Error.Message}}</div>
|
|
||||||
{{end}}
|
|
||||||
</form>
|
</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 (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/dgraph-io/badger/v2"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
"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/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"
|
"golang.org/x/exp/slog"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -24,10 +29,33 @@ var (
|
||||||
|
|
||||||
A web application that grabs the links to videos from over a
|
A web application that grabs the links to videos from over a
|
||||||
thousand websites using the yt-dlp project under the hood.`,
|
thousand websites using the yt-dlp project under the hood.`,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if err := app.Serve(cfg); err != nil {
|
logger := slog.Default()
|
||||||
slog.Error("Error when serving website", slog.String("error", err.Error()))
|
|
||||||
|
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().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("listen", "l", "", "address to listen on")
|
||||||
rootCmd.PersistentFlags().StringP("base-path", "b", "", "the base path, used when behind reverse proxy")
|
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")
|
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