147 lines
3.5 KiB
Go
147 lines
3.5 KiB
Go
package routes
|
|
|
|
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 Router() chi.Router {
|
|
r := chi.NewRouter()
|
|
r.Get("/", home)
|
|
r.Route("/download", func(r chi.Router) {
|
|
r.Get("/", download)
|
|
r.Head("/proxy", proxyDownload)
|
|
r.Get("/proxy", proxyDownload)
|
|
})
|
|
return r
|
|
}
|
|
|
|
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()
|
|
}
|