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