ytdl-web/app/controllers/download.go

181 lines
4.4 KiB
Go

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()
}