Compare commits
23 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
0e35f73f06 | |
|
|
c281da0eb0 | |
|
|
2630af4921 | |
|
|
f3f852fefd | |
|
|
668b36ad04 | |
|
|
b871ccdd7c | |
|
|
59fa1bd369 | |
|
|
d814432ebb | |
|
|
247cc211fa | |
|
|
681225c2ae | |
|
|
47caf17973 | |
|
|
36de48cc94 | |
|
|
63556e2983 | |
|
|
9dff066a9c | |
|
|
f6d1ecd757 | |
|
|
aa0f47607f | |
|
|
77c42dffd8 | |
|
|
9bba5db233 | |
|
|
c2b70e2ead | |
|
|
e59915a8f5 | |
|
|
74ea23add9 | |
|
|
06ba575c42 | |
|
|
8e4a9cb4f1 |
|
|
@ -3,7 +3,7 @@ testdata_dir = "testdata"
|
||||||
tmp_dir = "tmp"
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
args_bin = ["-l", "0.0.0.0"]
|
args_bin = ["-l", "0.0.0.0", "-p", "3000"]
|
||||||
bin = "./out/ytdl-web"
|
bin = "./out/ytdl-web"
|
||||||
cmd = "build"
|
cmd = "build"
|
||||||
delay = 1000
|
delay = 1000
|
||||||
|
|
|
||||||
13
.envrc
13
.envrc
|
|
@ -1,3 +1,12 @@
|
||||||
source_url "https://raw.githubusercontent.com/cachix/devenv/d1f7b48e35e6dee421cfd0f51481d17f77586997/direnvrc" "sha256-YBzqskFZxmNb3kYVoKD9ZixoPXJh1C9ZvTLGFRkauZ0="
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
use devenv
|
export DIRENV_WARN_TIMEOUT=20s
|
||||||
|
|
||||||
|
eval "$(devenv direnvrc)"
|
||||||
|
|
||||||
|
# `use devenv` supports the same options as the `devenv shell` command.
|
||||||
|
#
|
||||||
|
# To silence all output, use `--quiet`.
|
||||||
|
#
|
||||||
|
# Example usage: use devenv --quiet --impure --option services.postgres.enable:bool true
|
||||||
|
use devenv
|
||||||
|
|
|
||||||
|
|
@ -13,4 +13,25 @@ devenv.local.nix
|
||||||
.direnv
|
.direnv
|
||||||
|
|
||||||
# pre-commit
|
# pre-commit
|
||||||
.pre-commit-config.yaml
|
.pre-commit-config.yaml
|
||||||
|
# Devenv
|
||||||
|
.devenv*
|
||||||
|
devenv.local.nix
|
||||||
|
devenv.local.yaml
|
||||||
|
|
||||||
|
# direnv
|
||||||
|
.direnv
|
||||||
|
|
||||||
|
# pre-commit
|
||||||
|
.pre-commit-config.yaml
|
||||||
|
|
||||||
|
# Devenv
|
||||||
|
.devenv*
|
||||||
|
devenv.local.nix
|
||||||
|
devenv.local.yaml
|
||||||
|
|
||||||
|
# direnv
|
||||||
|
.direnv
|
||||||
|
|
||||||
|
# pre-commit
|
||||||
|
.pre-commit-config.yaml
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"ansible.python.interpreterPath": "/bin/python"
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
ARG GOLANG_VERSION="1.20.11"
|
ARG GOLANG_VERSION="1.22.1"
|
||||||
ARG DEBIAN_VERSION="bookworm"
|
ARG DEBIAN_VERSION="bookworm"
|
||||||
|
|
||||||
FROM golang:${GOLANG_VERSION}-${DEBIAN_VERSION} AS build
|
FROM golang:${GOLANG_VERSION}-${DEBIAN_VERSION} AS build
|
||||||
|
|
@ -27,7 +27,7 @@ RUN go build \
|
||||||
|
|
||||||
FROM python:${DEBIAN_VERSION}
|
FROM python:${DEBIAN_VERSION}
|
||||||
|
|
||||||
ARG YTDLP_VERSION="2023.12.30"
|
ARG YTDLP_VERSION="2024.11.04"
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ You can configure the application using environment variables
|
||||||
| ---------------------------------- | ----------------------------------------------------------------------------------------------- | --------------- | ------------------------------------------------------------------------------ |
|
| ---------------------------------- | ----------------------------------------------------------------------------------------------- | --------------- | ------------------------------------------------------------------------------ |
|
||||||
| YTDL_CONFIGDIR | Add a custom config directory to search for the config file | ` ` | |
|
| YTDL_CONFIGDIR | Add a custom config directory to search for the config file | ` ` | |
|
||||||
| YTDL_ENV | The application environment | `Production` | `Development`, `Staging`, `Production` |
|
| YTDL_ENV | The application environment | `Production` | `Development`, `Staging`, `Production` |
|
||||||
| YTDL_BINARYPATH | The path to the yt-dlp binary | `yt-dlp` | |
|
| YTDL_YTDLP_BINARYPATH | The path to the yt-dlp binary | `yt-dlp` | |
|
||||||
| YTDL_HTTP_PORT | The tcp port for the web server to listen on | `8080` | |
|
| YTDL_HTTP_PORT | The tcp port for the web server to listen on | `8080` | |
|
||||||
| YTDL_HTTP_LISTEN | The address for the web server to listen on | `127.0.0.1` | `0.0.0.0`, `127.0.0.1`, etc. |
|
| YTDL_HTTP_LISTEN | The address for the web server to listen on | `127.0.0.1` | `0.0.0.0`, `127.0.0.1`, etc. |
|
||||||
| YTDL_HTTP_BASEPATH | The base path of the application, useful for reverse proxies | `/` | |
|
| YTDL_HTTP_BASEPATH | The base path of the application, useful for reverse proxies | `/` | |
|
||||||
|
|
@ -36,7 +36,7 @@ You can configure the application using environment variables
|
||||||
| YTDL_COOKIES_FROMBROWSER_BROWSER | The name of the browser to load cookies from. (if specified, it disables YTDL_COOKIES_FILEPATH) | ` ` | `brave`, `chrome`, `chromium`, `edge`, `firefox`, `opera`, `safari`, `vivaldi` |
|
| YTDL_COOKIES_FROMBROWSER_BROWSER | The name of the browser to load cookies from. (if specified, it disables YTDL_COOKIES_FILEPATH) | ` ` | `brave`, `chrome`, `chromium`, `edge`, `firefox`, `opera`, `safari`, `vivaldi` |
|
||||||
| YTDL_COOKIES_FROMBROWSER_KEYRING | The name of the keyring for decrypting cookies for the chromium browser on linux | ` ` | `basictext`, `gnomekeyring`, `kwallet` |
|
| YTDL_COOKIES_FROMBROWSER_KEYRING | The name of the keyring for decrypting cookies for the chromium browser on linux | ` ` | `basictext`, `gnomekeyring`, `kwallet` |
|
||||||
| YTDL_COOKIES_FROMBROWSER_PROFILE | The browser profile to load cookies from | ` ` | |
|
| YTDL_COOKIES_FROMBROWSER_PROFILE | The browser profile to load cookies from | ` ` | |
|
||||||
| YTDL_COOKIES_FROMBROWSER_CONTAINER | The container name (if firefox) top load the cookies from | ` ` | |
|
| YTDL_COOKIES_FROMBROWSER_CONTAINER | The container name (if firefox) to load the cookies from | ` ` | |
|
||||||
|
|
||||||
## Building from source
|
## Building from source
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,163 +0,0 @@
|
||||||
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/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...)
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
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/pkg/ytdl"
|
|
||||||
"go.fifitido.net/ytdl-web/version"
|
|
||||||
)
|
|
||||||
|
|
||||||
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)
|
|
||||||
isSecure := r.URL.Scheme == "https" || r.Header.Get("X-Forwarded-Proto") == "https"
|
|
||||||
|
|
||||||
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(),
|
|
||||||
"IsSecure": isSecure,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
app.Views.Render(w, "index", view.Data{
|
|
||||||
"BasePath": viper.GetString("base_path"),
|
|
||||||
"Version": version.Version,
|
|
||||||
"Build": version.Build,
|
|
||||||
"BinaryVersion": c.ytdl.Version(),
|
|
||||||
"IsSecure": isSecure,
|
|
||||||
}, "layouts/main")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
package models
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/samber/lo"
|
|
||||||
"go.fifitido.net/ytdl-web/pkg/ytdl/metadata"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Video struct {
|
|
||||||
Meta *metadata.Metadata
|
|
||||||
Formats []metadata.Format
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetVideosFromMetadata(meta *metadata.Metadata) []Video {
|
|
||||||
if meta.IsPlaylist() {
|
|
||||||
return lo.Map(meta.Entries, func(video metadata.Metadata, _ int) Video {
|
|
||||||
return GetVideosFromMetadata(&video)[0]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
formats := lo.Filter(meta.Formats, func(item metadata.Format, _ int) bool {
|
|
||||||
return item.ACodec != "none" && item.VCodec != "none" && item.Protocol != "m3u8_native"
|
|
||||||
})
|
|
||||||
|
|
||||||
for i, j := 0, len(formats)-1; i < j; i, j = i+1, j-1 {
|
|
||||||
formats[i], formats[j] = formats[j], formats[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
return []Video{
|
|
||||||
{
|
|
||||||
Meta: meta,
|
|
||||||
Formats: formats,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
49
app/views.go
49
app/views.go
|
|
@ -1,49 +0,0 @@
|
||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"embed"
|
|
||||||
"encoding/json"
|
|
||||||
"html/template"
|
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"github.com/htfy96/reformism"
|
|
||||||
"go.fifitido.net/ytdl-web/pkg/view/html"
|
|
||||||
"go.fifitido.net/ytdl-web/pkg/ytdl/metadata"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed views/*
|
|
||||||
var viewsfs embed.FS
|
|
||||||
|
|
||||||
var Views = html.New(
|
|
||||||
viewsfs,
|
|
||||||
html.DefaultOptions().
|
|
||||||
WithBaseDir("views").
|
|
||||||
WithFunction("unsafe", func(s string) template.HTML {
|
|
||||||
return template.HTML(s)
|
|
||||||
}).
|
|
||||||
WithFunction("queryEscape", func(s string) string {
|
|
||||||
return url.QueryEscape(s)
|
|
||||||
}).
|
|
||||||
WithFunction("jsonMarshal", func(s any) (string, error) {
|
|
||||||
j, err := json.MarshalIndent(s, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return string(j), nil
|
|
||||||
}).
|
|
||||||
WithFunction("downloadContext", func(meta metadata.Metadata, url, basePath string, format metadata.Format) map[string]any {
|
|
||||||
return map[string]any{
|
|
||||||
"Meta": meta,
|
|
||||||
"Url": url,
|
|
||||||
"BasePath": basePath,
|
|
||||||
"Format": format,
|
|
||||||
}
|
|
||||||
}).
|
|
||||||
WithFunctions(reformism.FuncsHTML),
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
if err := Views.Load(); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
<div class="d-flex flex-column align-items-center">
|
|
||||||
<h1>Download Video</h1>
|
|
||||||
<h2 class="fs-4 text-muted text-center">{{.Meta.Title}}</h2>
|
|
||||||
<p style="font-size: 0.85rem">{{.Url}}</p>
|
|
||||||
<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
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{$root := .}} {{range $vidIndex, $video := .Videos}} {{if not (eq $vidIndex
|
|
||||||
0)}}
|
|
||||||
<hr class="mt-5" />
|
|
||||||
{{end}}
|
|
||||||
<div class="d-flex flex-column flex-lg-row justify-content-center gap-5 mt-5">
|
|
||||||
<div class="d-flex justify-content-center">
|
|
||||||
<img
|
|
||||||
src="{{.Meta.Thumbnail}}"
|
|
||||||
alt="{{.Meta.Title}}"
|
|
||||||
style="max-height: 25rem; max-width: 100%; margin: 0 auto"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="downloads flex-lg-grow-1">
|
|
||||||
{{range $index, $format := $video.Formats}}
|
|
||||||
<div style="font-size: smaller">{{$format.Format}}</div>
|
|
||||||
<div class="flex-grow-1 d-flex gap-3">
|
|
||||||
<a
|
|
||||||
class="btn btn-primary flex-grow-1"
|
|
||||||
download="{{$root.Meta.ID}}-{{$format.Resolution}}.{{$format.Ext}}"
|
|
||||||
P
|
|
||||||
href="{{$format.Url}}"
|
|
||||||
>
|
|
||||||
Download (direct)
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
class="btn btn-primary flex-grow-1"
|
|
||||||
download="{{$root.Meta.ID}}-{{$format.Resolution}}.{{$format.Ext}}"
|
|
||||||
href="{{$root.BasePath}}/download/proxy?url={{queryEscape $root.Url}}&format={{$format.FormatID}}&index={{$vidIndex}}"
|
|
||||||
>
|
|
||||||
Download (proxied)
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
<h1 class="text-center">YTDL Web</h1>
|
|
||||||
<p class="text-center mb-5">
|
|
||||||
Download videos from over a thousand websites with the help of
|
|
||||||
<a href="https://github.com/yt-dlp/yt-dlp">yt-dlp</a>, a fork of youtube-dl
|
|
||||||
with more features and fixes.
|
|
||||||
<br />
|
|
||||||
View a complete list of supported websites
|
|
||||||
<a href="https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md"
|
|
||||||
>here</a
|
|
||||||
>.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<form
|
|
||||||
hx-get="{{.BasePath}}/download"
|
|
||||||
hx-trigger="submit"
|
|
||||||
hx-target="#main-content"
|
|
||||||
hx-swap="innerHTML"
|
|
||||||
>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="url" class="form-label visually-hidden">Url</label>
|
|
||||||
<div class="input-group">
|
|
||||||
<input
|
|
||||||
type="url"
|
|
||||||
name="url"
|
|
||||||
id="url"
|
|
||||||
class="form-control"
|
|
||||||
required
|
|
||||||
placeholder="Enter url here then click download"
|
|
||||||
/>
|
|
||||||
{{if .IsSecure}}
|
|
||||||
<button
|
|
||||||
id="paste-button"
|
|
||||||
class="btn btn-outline-secondary"
|
|
||||||
type="button"
|
|
||||||
title="Paste"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
style="width: 1.5rem; height: 1.5rem"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-grid">
|
|
||||||
<button type="submit" class="btn btn-primary">
|
|
||||||
Download
|
|
||||||
<div
|
|
||||||
class="spinner-border spinner-border-sm htmx-indicator ms-1"
|
|
||||||
role="status"
|
|
||||||
>
|
|
||||||
<span class="visually-hidden">Loading...</span>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</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}}
|
|
||||||
|
|
@ -1,94 +0,0 @@
|
||||||
<!doctype html>
|
|
||||||
<html lang="en" class="h-100">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>YTDL Web</title>
|
|
||||||
<link
|
|
||||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css"
|
|
||||||
rel="stylesheet"
|
|
||||||
integrity="sha384-KK94CHFLLe+nY2dmCWGMq91rCGa5gtU4mk92HdvYe+M/SXH301p5ILy+dN9+nJOZ"
|
|
||||||
crossorigin="anonymous"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="stylesheet"
|
|
||||||
href="https://cdn.jsdelivr.net/npm/toastr@2.1.4/build/toastr.min.css"
|
|
||||||
integrity="sha256-R91pD48xW+oHbpJYGn5xR0Q7tMhH4xOrWn1QqMRINtA="
|
|
||||||
crossorigin="anonymous"
|
|
||||||
/>
|
|
||||||
<script
|
|
||||||
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js"
|
|
||||||
integrity="sha384-ENjdO4Dr2bkBIFxQpeoTz1HIcje39Wm4jDKdf19U8gI4ddQ3GYNS7NTKfAdVQSZe"
|
|
||||||
crossorigin="anonymous"
|
|
||||||
defer
|
|
||||||
></script>
|
|
||||||
<script
|
|
||||||
src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.4/jquery.min.js"
|
|
||||||
defer
|
|
||||||
></script>
|
|
||||||
<script
|
|
||||||
src="https://cdn.jsdelivr.net/npm/toastr@2.1.4/build/toastr.min.js"
|
|
||||||
integrity="sha256-Hgwq1OBpJ276HUP9H3VJkSv9ZCGRGQN+JldPJ8pNcUM="
|
|
||||||
crossorigin="anonymous"
|
|
||||||
defer
|
|
||||||
></script>
|
|
||||||
<script
|
|
||||||
src="https://cdn.jsdelivr.net/npm/htmx.org@1.9.4/dist/htmx.min.js"
|
|
||||||
integrity="sha256-XIivRAE99i/eil5P31JNihaDSiix0V40rgmUrCfNTH4="
|
|
||||||
crossorigin="anonymous"
|
|
||||||
></script>
|
|
||||||
<style>
|
|
||||||
#toast-container > div {
|
|
||||||
-moz-box-shadow: none !important;
|
|
||||||
-webkit-box-shadow: none !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
-ms-filter: none;
|
|
||||||
filter: none;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.downloads {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(auto, max-content) auto;
|
|
||||||
gap: 1.5rem;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.see-more-btn,
|
|
||||||
.collapse {
|
|
||||||
grid-column: span 2;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body class="d-flex flex-column h-100" data-bs-theme="dark">
|
|
||||||
<div class="flex-shrink-0">
|
|
||||||
{{template "partials/navbar" .}}
|
|
||||||
<main id="main-content" class="container my-5">{{yield}}</main>
|
|
||||||
</div>
|
|
||||||
{{template "partials/footer" .}}
|
|
||||||
<script>
|
|
||||||
/**
|
|
||||||
* @param content {Element}
|
|
||||||
*/
|
|
||||||
function setupPaste(content) {
|
|
||||||
const pasteButton = content.querySelector("#paste-button");
|
|
||||||
const urlField = content.querySelector("#url");
|
|
||||||
|
|
||||||
if (pasteButton) {
|
|
||||||
pasteButton.addEventListener("click", async () => {
|
|
||||||
try {
|
|
||||||
const text = await navigator.clipboard.readText();
|
|
||||||
urlField.value = text;
|
|
||||||
} catch (error) {
|
|
||||||
toastr.error("Failed to paste url from clipboard.");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
htmx.onLoad(setupPaste);
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
<footer class="footer mt-auto py-3 bg-body-tertiary" style="font-size: 0.95rem">
|
|
||||||
<div class="container">
|
|
||||||
<div class="row gap-2 gap-md-0">
|
|
||||||
<div class="d-flex gap-4 col-md justify-content-center">
|
|
||||||
<div class="d-flex gap-1 align-items-baseline">
|
|
||||||
Version:
|
|
||||||
<span class="text-muted">{{.Version}}</span>
|
|
||||||
<span class="text-muted text-nowrap" style="font-size: smaller"
|
|
||||||
>(Build: {{.Build}})</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex gap-1 align-items-baseline text-nowrap">
|
|
||||||
yt-dlp version: <span class="text-muted">{{.BinaryVersion}}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex gap-2 col-md justify-content-center">
|
|
||||||
<a href="https://git.fifitido.net/apps/ytdl-web">Git Repository</a>
|
|
||||||
<span class="text-muted">© 2023 FiFiTiDo</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
<nav class="navbar navbar-dark bg-primary">
|
|
||||||
<div class="container">
|
|
||||||
<a href="/" class="navbar-brand">YTDL Web</a>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
127
cmd/root.go
127
cmd/root.go
|
|
@ -1,127 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright © 2023 Evan Fiordeliso <evan.fiordeliso@gmail.com>
|
|
||||||
*/
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/dgraph-io/badger/v2"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
"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/pkg/utils"
|
|
||||||
"go.fifitido.net/ytdl-web/pkg/ytdl"
|
|
||||||
"go.fifitido.net/ytdl-web/pkg/ytdl/cache"
|
|
||||||
"golang.org/x/exp/slog"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
cfgFile string
|
|
||||||
cfg *config.Config
|
|
||||||
|
|
||||||
rootCmd = &cobra.Command{
|
|
||||||
Use: "ytdl-web",
|
|
||||||
Short: "A web frontend for yt-dlp",
|
|
||||||
Long: `YTDL Web
|
|
||||||
|
|
||||||
A web application that grabs the links to videos from over a
|
|
||||||
thousand websites using the yt-dlp project under the hood.`,
|
|
||||||
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.
|
|
||||||
DefaultOptions().
|
|
||||||
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()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func Execute() {
|
|
||||||
err := rootCmd.Execute()
|
|
||||||
if err != nil {
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
cobra.OnInitialize(initConfig)
|
|
||||||
|
|
||||||
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $XDG_CONFIG_HOME/ytdl-web/config.yml)")
|
|
||||||
|
|
||||||
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")
|
|
||||||
rootCmd.PersistentFlags().BoolP("cookies-enabled", "C", false, "whether cookies are enabled")
|
|
||||||
rootCmd.PersistentFlags().StringP("cookies", "c", "", "the path to the cookies file")
|
|
||||||
|
|
||||||
// trunk-ignore-begin(golangci-lint/errcheck): Ignoring errors
|
|
||||||
viper.BindPFlag("http.port", rootCmd.PersistentFlags().Lookup("port"))
|
|
||||||
viper.BindPFlag("http.listen", rootCmd.PersistentFlags().Lookup("listen"))
|
|
||||||
viper.BindPFlag("http.basePath", rootCmd.PersistentFlags().Lookup("base-path"))
|
|
||||||
viper.BindPFlag("ytdlp.binaryPath", rootCmd.PersistentFlags().Lookup("ytdlp-path"))
|
|
||||||
viper.BindPFlag("cookies.enabled", rootCmd.PersistentFlags().Lookup("cookies-enabled"))
|
|
||||||
viper.BindPFlag("cookies.filePath", rootCmd.PersistentFlags().Lookup("cookies"))
|
|
||||||
// trunk-ignore-end(golangci-lint/errcheck)
|
|
||||||
}
|
|
||||||
|
|
||||||
func initConfig() {
|
|
||||||
var err error
|
|
||||||
if cfgFile != "" {
|
|
||||||
cfg, err = config.LoadConfig(cfgFile)
|
|
||||||
} else {
|
|
||||||
cfg, err = config.LoadConfig()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Error loading configuration", slog.String("error", err.Error()))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
initLogging()
|
|
||||||
|
|
||||||
slog.Info("Configuration loaded")
|
|
||||||
}
|
|
||||||
|
|
||||||
func initLogging() {
|
|
||||||
var handler slog.Handler
|
|
||||||
|
|
||||||
if cfg.IsProduction() {
|
|
||||||
handler = slog.HandlerOptions{
|
|
||||||
AddSource: true,
|
|
||||||
Level: slog.LevelInfo,
|
|
||||||
}.NewJSONHandler(os.Stdout)
|
|
||||||
} else {
|
|
||||||
handler = slog.HandlerOptions{
|
|
||||||
AddSource: true,
|
|
||||||
Level: slog.LevelDebug,
|
|
||||||
}.NewTextHandler(os.Stdout)
|
|
||||||
}
|
|
||||||
|
|
||||||
slog.SetDefault(slog.New(handler))
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,158 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"mime"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"go.fifitido.net/ytdl-web/pkg/views"
|
||||||
|
"go.fifitido.net/ytdl-web/pkg/ytdl"
|
||||||
|
"go.fifitido.net/ytdl-web/pkg/ytdl/metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
mime.AddExtensionType(".mp4", "video/mp4")
|
||||||
|
mime.AddExtensionType(".m4v", "video/x-m4v")
|
||||||
|
mime.AddExtensionType(".mkv", "video/x-matroska")
|
||||||
|
mime.AddExtensionType(".webm", "video/webm")
|
||||||
|
mime.AddExtensionType(".mov", "video/quicktime")
|
||||||
|
mime.AddExtensionType(".avi", "video/x-msvideo")
|
||||||
|
mime.AddExtensionType(".wmv", "video/x-ms-wmv")
|
||||||
|
mime.AddExtensionType(".mpg", "video/mpeg")
|
||||||
|
mime.AddExtensionType(".flv", "video/x-flv")
|
||||||
|
mime.AddExtensionType(".3gp", "video/3gpp")
|
||||||
|
mime.AddExtensionType(".m3u8", "application/x-mpegURL")
|
||||||
|
mime.AddExtensionType(".ts", "video/mp2t")
|
||||||
|
mime.AddExtensionType(".m4a", "audio/mp4")
|
||||||
|
mime.AddExtensionType(".mp3", "audio/mpeg")
|
||||||
|
mime.AddExtensionType(".aac", "audio/aac")
|
||||||
|
mime.AddExtensionType(".ogg", "audio/ogg")
|
||||||
|
mime.AddExtensionType(".wav", "audio/wav")
|
||||||
|
mime.AddExtensionType(".opus", "audio/opus")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUrlParam(r *http.Request) (string, error) {
|
||||||
|
urlRaw := r.URL.Query().Get("url")
|
||||||
|
if urlRaw == "" {
|
||||||
|
return "", errors.New("url param not specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
urlUnescaped, err := url.QueryUnescape(urlRaw)
|
||||||
|
if err != nil || len(urlUnescaped) < 1 {
|
||||||
|
return "", errors.New("invalid url")
|
||||||
|
}
|
||||||
|
|
||||||
|
return urlUnescaped, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func download(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ytdl := ytdl.Default()
|
||||||
|
|
||||||
|
videoUrl, err := getUrlParam(r)
|
||||||
|
if err != nil {
|
||||||
|
slog.ErrorContext(r.Context(), "Failed to get video url param", slog.Any("error", err))
|
||||||
|
views.Render(w, r, views.Home(&views.Error{Message: "Invalid URL"}))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
meta, err := ytdl.GetMetadata(r.Context(), videoUrl)
|
||||||
|
if err != nil {
|
||||||
|
views.Render(w, r, views.Home(&views.Error{Message: "Could not find a video at that url", RetryUrl: &videoUrl}))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
views.Render(w, r, views.Downloads(&views.DownloadsViewModel{Url: videoUrl, Meta: meta}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func proxyDownload(w http.ResponseWriter, r *http.Request) {
|
||||||
|
flusher, ok := w.(http.Flusher)
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, "streaming not supported", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ytdl := ytdl.Default()
|
||||||
|
videoUrl, err := getUrlParam(r)
|
||||||
|
if err != nil {
|
||||||
|
slog.ErrorContext(r.Context(), "Failed to get video url param", slog.Any("error", err))
|
||||||
|
http.Error(w, "Video URL not specified or invalid URL", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
formatId := r.URL.Query().Get("format")
|
||||||
|
if formatId == "" {
|
||||||
|
http.Error(w, "Format ID not specified", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
meta, err := ytdl.GetMetadata(r.Context(), 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 := views.GetVideosFromMetadata(meta)
|
||||||
|
|
||||||
|
index, err := strconv.Atoi(r.URL.Query().Get("index"))
|
||||||
|
if err != nil || index < 0 || index >= len(videos) {
|
||||||
|
slog.Error("Failed to parse index", slog.Any("error", err), slog.Int("index", index), slog.Int("len", len(videos)))
|
||||||
|
http.Error(w, "Index not specified or invalid index", 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 {
|
||||||
|
for _, f := range video.OtherFormats {
|
||||||
|
if f.FormatID == formatId {
|
||||||
|
format = &f
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if format == nil {
|
||||||
|
slog.Error("Failed to find format", slog.String("format_id", formatId), slog.String("video_id", video.Meta.ID), slog.Int("index", index))
|
||||||
|
http.Error(w, "Format not found", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.%s\"", meta.ID, format.Ext))
|
||||||
|
w.Header().Set("Transfer-Encoding", "chunked")
|
||||||
|
w.Header().Set("Content-Type", mime.TypeByExtension("."+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()
|
||||||
|
defer write.Close()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer write.Close()
|
||||||
|
ytdl.Download(r.Context(), write, videoUrl, format.FormatID, index)
|
||||||
|
}()
|
||||||
|
|
||||||
|
if _, err := io.Copy(w, read); err != nil {
|
||||||
|
slog.Error("Failed to copy", slog.String("error", err.Error()))
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
flusher.Flush()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"github.com/dgraph-io/badger/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type badgerLogger struct {
|
||||||
|
logger *slog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ badger.Logger = (*badgerLogger)(nil)
|
||||||
|
|
||||||
|
// Debugf implements badger.Logger
|
||||||
|
func (l *badgerLogger) Debugf(f string, a ...any) {
|
||||||
|
l.logger.Debug(fmt.Sprintf(f, a...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errorf implements badger.Logger
|
||||||
|
func (l *badgerLogger) Errorf(f string, a ...any) {
|
||||||
|
l.logger.Error(fmt.Sprintf(f, a...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Infof implements badger.Logger
|
||||||
|
func (l *badgerLogger) Infof(f string, a ...any) {
|
||||||
|
l.logger.Info(fmt.Sprintf(f, a...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warningf implements badger.Logger
|
||||||
|
func (l *badgerLogger) Warningf(f string, a ...any) {
|
||||||
|
l.logger.Warn(fmt.Sprintf(f, a...))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
/*
|
||||||
|
Copyright © 2024 Evan Fiordeliso <evan.fiordeliso@gmail.com>
|
||||||
|
*/
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/dgraph-io/badger/v2"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
|
slogchi "github.com/samber/slog-chi"
|
||||||
|
"go.fifitido.net/ytdl-web/pkg/config"
|
||||||
|
"go.fifitido.net/ytdl-web/pkg/serverctx"
|
||||||
|
"go.fifitido.net/ytdl-web/pkg/views"
|
||||||
|
"go.fifitido.net/ytdl-web/pkg/ytdl"
|
||||||
|
"go.fifitido.net/ytdl-web/pkg/ytdl/cache"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
env = config.String("env", "production", "the environment to run")
|
||||||
|
listenAddr = config.String("listen", ":8080", "address to listen on")
|
||||||
|
basePath = config.String("base_path", "/", "the base path, used when behind reverse proxy")
|
||||||
|
ytdlpPath = config.String("ytdlp.binary", "", "path to yt-dlp executable")
|
||||||
|
cacheTTL = config.Duration("cache.ttl", time.Hour, "the TTL for the cache")
|
||||||
|
cacheDir = config.String("cache.dir", "", "the directory to store the cache")
|
||||||
|
cookies = config.Bool("cookies", false, "whether cookies are enabled")
|
||||||
|
cookiesPath = config.String("cookies.path", "", "the path to the cookies file")
|
||||||
|
cookiesBrowser = config.String("cookies.browser", "", "the browser to use for cookies")
|
||||||
|
cookiesBrowserKeyring = config.String("cookies.browser.keyring", "", "the keyring to use for cookies")
|
||||||
|
cookiesBrowserProfile = config.String("cookies.browser.profile", "", "the profile to use for cookies")
|
||||||
|
cookiesBrowserContainer = config.String("cookies.browser.container", "", "the container to use for cookies")
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
|
||||||
|
AddSource: true,
|
||||||
|
Level: slog.LevelInfo,
|
||||||
|
}))
|
||||||
|
|
||||||
|
if env() == "development" {
|
||||||
|
logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
||||||
|
AddSource: true,
|
||||||
|
Level: slog.LevelDebug,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.SetDefault(logger)
|
||||||
|
|
||||||
|
db, err := badger.Open(
|
||||||
|
badger.
|
||||||
|
DefaultOptions(cacheDir()).
|
||||||
|
WithLogger(&badgerLogger{logger: logger.With("module", "badger")}),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
cache := cache.NewDefaultMetadataCache(db, cacheTTL())
|
||||||
|
ytdl.SetDefault(
|
||||||
|
ytdl.NewYtdl(&ytdl.Config{
|
||||||
|
BinaryPath: ytdlpPath(),
|
||||||
|
CookiesEnabled: cookies(),
|
||||||
|
CookiesFilePath: cookiesPath(),
|
||||||
|
CookiesBrowser: cookiesBrowser(),
|
||||||
|
CookiesBrowserKeyring: cookiesBrowserKeyring(),
|
||||||
|
CookiesBrowserProfile: cookiesBrowserProfile(),
|
||||||
|
CookiesBrowserContainer: cookiesBrowserContainer(),
|
||||||
|
}, logger, cache),
|
||||||
|
)
|
||||||
|
|
||||||
|
r := chi.NewRouter()
|
||||||
|
|
||||||
|
r.Use(
|
||||||
|
serverctx.Middleware(basePath()),
|
||||||
|
middleware.RequestID,
|
||||||
|
middleware.RealIP,
|
||||||
|
slogchi.New(logger),
|
||||||
|
middleware.Recoverer,
|
||||||
|
)
|
||||||
|
|
||||||
|
r.Route(basePath(), func(r chi.Router) {
|
||||||
|
r.Get("/", views.Handler(views.Home(nil)))
|
||||||
|
r.Route("/download", func(r chi.Router) {
|
||||||
|
r.Get("/", download)
|
||||||
|
r.Head("/proxy", proxyDownload)
|
||||||
|
r.Get("/proxy", proxyDownload)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.Info("Starting HTTP server", slog.String("listen", listenAddr()))
|
||||||
|
|
||||||
|
if err := http.ListenAndServe(listenAddr(), r); err != nil {
|
||||||
|
logger.Error("failed to serve website", slog.Any("error", err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
---
|
|
||||||
# The server environment
|
|
||||||
# For dev environments use Development
|
|
||||||
# For prod environments use Production
|
|
||||||
# For staging envronments use Staging
|
|
||||||
env: Production
|
|
||||||
# The path to the yt-dlp binary
|
|
||||||
# If it is already in your $PATH just yt-dlp will work.
|
|
||||||
binaryPath: yt-dlp
|
|
||||||
http:
|
|
||||||
# The port to listen on
|
|
||||||
port: 8080
|
|
||||||
# The address to listen on
|
|
||||||
# For local only access use 127.0.0.1
|
|
||||||
# For public access use 0.0.0.0
|
|
||||||
listen: 0.0.0.0
|
|
||||||
# The base path of the application, useful for reverse proxies
|
|
||||||
basePath: ""
|
|
||||||
|
|
||||||
# A list of proxy servers to trust for security purposes
|
|
||||||
# Only needed when accessing app behind a proxy
|
|
||||||
trustedProxies: []
|
|
||||||
cookies:
|
|
||||||
# Whether to use cookies when fetching the video metadata
|
|
||||||
enabled: false
|
|
||||||
|
|
||||||
# The path to the netscape formatted cookies file
|
|
||||||
# See: https://www.reddit.com/r/youtubedl/wiki/cookies/ for details.
|
|
||||||
filePath: ~/.cookies
|
|
||||||
|
|
||||||
# Settings for using cookies from a browser's cookies store
|
|
||||||
fromBrowser:
|
|
||||||
# The name of the browser to load cookies from.
|
|
||||||
# Currently supported browsers are: brave, chrome, chromium, edge,
|
|
||||||
# firefox, opera, safari, vivaldi.
|
|
||||||
browser: firefox
|
|
||||||
|
|
||||||
# The keyring used for decrypting Chromium cookies on Linux
|
|
||||||
# Currently supported keyrings are: basictext, gnomekeyring, kwallet
|
|
||||||
keyring: basictext
|
|
||||||
|
|
||||||
# The profile to load cookies from (Firefox)
|
|
||||||
profile: default
|
|
||||||
|
|
||||||
# The container to load cookies from (Firefox)
|
|
||||||
container: none
|
|
||||||
123
config/config.go
123
config/config.go
|
|
@ -1,123 +0,0 @@
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/adrg/xdg"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
Env string `mapstructure:"env"`
|
|
||||||
BinaryPath string `mapstructure:"binaryPath"`
|
|
||||||
HTTP ConfigHTTP `mapstructure:"http"`
|
|
||||||
Cache ConfigCache `mapstructure:"cache"`
|
|
||||||
Cookies ConfigCookies `mapstructure:"cookies"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) IsProduction() bool {
|
|
||||||
return c.Env == "Production"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) IsDevelopment() bool {
|
|
||||||
return c.Env == "Development"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) IsStaging() bool {
|
|
||||||
return c.Env == "Staging"
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConfigHTTP struct {
|
|
||||||
Port int `mapstructure:"port"`
|
|
||||||
Listen string `mapstructure:"listen"`
|
|
||||||
BasePath string `mapstructure:"basePath"`
|
|
||||||
TrustedProxies []string `mapstructure:"trustedProxies"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConfigCache struct {
|
|
||||||
TTL time.Duration `mapstructure:"ttl"`
|
|
||||||
DirPath string `mapstructure:"dirPath"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConfigCookies struct {
|
|
||||||
Enabled bool `mapstructure:"enabled"`
|
|
||||||
FilePath string `mapstructure:"filePath"`
|
|
||||||
FromBrowser ConfigCookiesFromBrowser `mapstructure:"fromBrowser"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConfigCookiesFromBrowser struct {
|
|
||||||
Browser string `mapstructure:"browser"`
|
|
||||||
Keyring string `mapstructure:"keyring"`
|
|
||||||
Profile string `mapstructure:"profile"`
|
|
||||||
Container string `mapstructure:"container"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func DefaultConfig() *Config {
|
|
||||||
return &Config{
|
|
||||||
Env: "Production",
|
|
||||||
BinaryPath: "yt-dlp",
|
|
||||||
HTTP: ConfigHTTP{
|
|
||||||
Port: 8080,
|
|
||||||
Listen: "127.0.0.1",
|
|
||||||
BasePath: "/",
|
|
||||||
},
|
|
||||||
Cache: ConfigCache{
|
|
||||||
TTL: time.Hour,
|
|
||||||
DirPath: "/tmp/ytdl-web",
|
|
||||||
},
|
|
||||||
Cookies: ConfigCookies{
|
|
||||||
Enabled: false,
|
|
||||||
FilePath: "./cookies.txt",
|
|
||||||
FromBrowser: ConfigCookiesFromBrowser{},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func LoadConfig(paths ...string) (*Config, error) {
|
|
||||||
v := viper.New()
|
|
||||||
|
|
||||||
v.SetEnvPrefix("YTDL")
|
|
||||||
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
|
||||||
v.AutomaticEnv()
|
|
||||||
|
|
||||||
v.SetConfigName("config")
|
|
||||||
v.SetConfigType("yaml")
|
|
||||||
|
|
||||||
if len(paths) > 0 {
|
|
||||||
for _, path := range paths {
|
|
||||||
v.AddConfigPath(path)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
envDir := os.Getenv("YTDL_CONFIGDIR")
|
|
||||||
if envDir != "" {
|
|
||||||
v.AddConfigPath(envDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
v.AddConfigPath(".")
|
|
||||||
|
|
||||||
homeDir, err := os.UserHomeDir()
|
|
||||||
if err == nil {
|
|
||||||
v.AddConfigPath(homeDir + "/.config/ytdl-web")
|
|
||||||
}
|
|
||||||
|
|
||||||
v.AddConfigPath(xdg.ConfigHome + "/ytdl-web")
|
|
||||||
for _, dir := range xdg.ConfigDirs {
|
|
||||||
v.AddConfigPath(dir + "/ytdl-web")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := v.ReadInConfig(); err != nil {
|
|
||||||
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
config := DefaultConfig()
|
|
||||||
if err := v.Unmarshal(config); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return config, nil
|
|
||||||
}
|
|
||||||
109
devenv.lock
109
devenv.lock
|
|
@ -3,11 +3,10 @@
|
||||||
"devenv": {
|
"devenv": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"dir": "src/modules",
|
"dir": "src/modules",
|
||||||
"lastModified": 1689175844,
|
"lastModified": 1762791812,
|
||||||
"narHash": "sha256-+ZAcAnogqNXz5P2/NiZonmgUiv+vCC7/swiSepyTulc=",
|
|
||||||
"owner": "cachix",
|
"owner": "cachix",
|
||||||
"repo": "devenv",
|
"repo": "devenv",
|
||||||
"rev": "db59403d5bdad71dce137705ed7cb926681e5f95",
|
"rev": "1faab0d28c573f2a8dba2cf457b9d383adba252a",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -20,11 +19,10 @@
|
||||||
"flake-compat": {
|
"flake-compat": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1673956053,
|
"lastModified": 1761588595,
|
||||||
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
|
|
||||||
"owner": "edolstra",
|
"owner": "edolstra",
|
||||||
"repo": "flake-compat",
|
"repo": "flake-compat",
|
||||||
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
|
"rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -33,37 +31,39 @@
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"flake-utils": {
|
"git-hooks": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"systems": "systems"
|
"flake-compat": "flake-compat",
|
||||||
|
"gitignore": "gitignore",
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1685518550,
|
"lastModified": 1762441963,
|
||||||
"narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=",
|
"owner": "cachix",
|
||||||
"owner": "numtide",
|
"repo": "git-hooks.nix",
|
||||||
"repo": "flake-utils",
|
"rev": "8e7576e79b88c16d7ee3bbd112c8d90070832885",
|
||||||
"rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef",
|
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "numtide",
|
"owner": "cachix",
|
||||||
"repo": "flake-utils",
|
"repo": "git-hooks.nix",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"gitignore": {
|
"gitignore": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
"pre-commit-hooks",
|
"git-hooks",
|
||||||
"nixpkgs"
|
"nixpkgs"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1660459072,
|
"lastModified": 1709087332,
|
||||||
"narHash": "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=",
|
|
||||||
"owner": "hercules-ci",
|
"owner": "hercules-ci",
|
||||||
"repo": "gitignore.nix",
|
"repo": "gitignore.nix",
|
||||||
"rev": "a20de23b925fd8264fd7fad6454652e142fd7f73",
|
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -74,80 +74,27 @@
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1689168768,
|
"lastModified": 1761313199,
|
||||||
"narHash": "sha256-mCw3LPg2jJkapvJpkd1IZ8k0IJlSG2ECvz3vcOAu+Uo=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "6fd9edc94426a3c050ad589c8f033b5ca55454c7",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"ref": "nixpkgs-unstable",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs-stable": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1685801374,
|
|
||||||
"narHash": "sha256-otaSUoFEMM+LjBI1XL/xGB5ao6IwnZOXc47qhIgJe8U=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "c37ca420157f4abc31e26f436c1145f8951ff373",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"ref": "nixos-23.05",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pre-commit-hooks": {
|
|
||||||
"inputs": {
|
|
||||||
"flake-compat": "flake-compat",
|
|
||||||
"flake-utils": "flake-utils",
|
|
||||||
"gitignore": "gitignore",
|
|
||||||
"nixpkgs": [
|
|
||||||
"nixpkgs"
|
|
||||||
],
|
|
||||||
"nixpkgs-stable": "nixpkgs-stable"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1688596063,
|
|
||||||
"narHash": "sha256-9t7RxBiKWHygsqXtiNATTJt4lim/oSYZV3RG8OjDDng=",
|
|
||||||
"owner": "cachix",
|
"owner": "cachix",
|
||||||
"repo": "pre-commit-hooks.nix",
|
"repo": "devenv-nixpkgs",
|
||||||
"rev": "c8d18ba345730019c3faf412c96a045ade171895",
|
"rev": "d1c30452ebecfc55185ae6d1c983c09da0c274ff",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "cachix",
|
"owner": "cachix",
|
||||||
"repo": "pre-commit-hooks.nix",
|
"ref": "rolling",
|
||||||
|
"repo": "devenv-nixpkgs",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"devenv": "devenv",
|
"devenv": "devenv",
|
||||||
|
"git-hooks": "git-hooks",
|
||||||
"nixpkgs": "nixpkgs",
|
"nixpkgs": "nixpkgs",
|
||||||
"pre-commit-hooks": "pre-commit-hooks"
|
"pre-commit-hooks": [
|
||||||
}
|
"git-hooks"
|
||||||
},
|
]
|
||||||
"systems": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681028828,
|
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"type": "github"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
132
devenv.nix
132
devenv.nix
|
|
@ -1,16 +1,23 @@
|
||||||
{ pkgs, ... }:
|
{ pkgs, config, ... }:
|
||||||
|
|
||||||
{
|
{
|
||||||
# https://devenv.sh/basics/
|
# https://devenv.sh/basics/
|
||||||
env.NAME = "ytdl-web";
|
env = {
|
||||||
env.BINARY_OUT = "./out/ytdl-web";
|
NAME = "ytdl-web";
|
||||||
env.VERSION = "v1.1.4";
|
BINARY_OUT = "./out/ytdl-web";
|
||||||
env.VERSION_PKG = "go.fifitido.net/ytdl-web/version";
|
VERSION = "v1.2.3";
|
||||||
env.DOCKER_REGISTRY = "git.fifitido.net";
|
VERSION_PKG = "go.fifitido.net/ytdl-web/version";
|
||||||
env.DOCKER_ORG = "apps";
|
DOCKER_REGISTRY = "git.fifitido.net";
|
||||||
env.DOCKER_PLATFORMS = "linux/amd64,linux/arm64";
|
DOCKER_ORG = "apps";
|
||||||
|
DOCKER_PLATFORMS = "linux/amd64,linux/arm64";
|
||||||
|
|
||||||
env.YTDL_ENV = "Development";
|
YTDL_WEB_ENV = "development";
|
||||||
|
YTDL_WEB_YTDLP_BINARY = "/usr/bin/yt-dlp";
|
||||||
|
YTDL_WEB_LISTEN = ":8989";
|
||||||
|
YTDL_WEB_CACHE_DIR = config.devenv.runtime + "/cache";
|
||||||
|
YTDL_WEB_COOKIES = "true";
|
||||||
|
YTDL_WEB_COOKIES_BROWSER = "firefox";
|
||||||
|
};
|
||||||
|
|
||||||
# https://devenv.sh/packages/
|
# https://devenv.sh/packages/
|
||||||
packages = with pkgs; [
|
packages = with pkgs; [
|
||||||
|
|
@ -22,65 +29,68 @@
|
||||||
buildkit
|
buildkit
|
||||||
docker-buildx
|
docker-buildx
|
||||||
yt-dlp
|
yt-dlp
|
||||||
|
cobra-cli
|
||||||
];
|
];
|
||||||
|
|
||||||
# https://devenv.sh/scripts/
|
# https://devenv.sh/scripts/
|
||||||
scripts.deps.exec = "go mod download";
|
scripts = {
|
||||||
scripts.tidy.exec = "go mod tidy";
|
deps.exec = "go mod download";
|
||||||
scripts.check.exec = "goreleaser check";
|
tidy.exec = "go mod tidy";
|
||||||
|
check.exec = "goreleaser check";
|
||||||
|
|
||||||
scripts.build-id.exec = "git rev-parse --short HEAD";
|
build-id.exec = "git rev-parse --short HEAD";
|
||||||
scripts.build-date.exec = "date -Iseconds";
|
build-date.exec = "date -Iseconds";
|
||||||
|
|
||||||
scripts.build.exec = ''
|
build.exec = ''
|
||||||
go build -ldflags="
|
go build -ldflags="
|
||||||
-X $VERSION_PKG.Version=$VERSION
|
-X $VERSION_PKG.Version=$VERSION
|
||||||
-X $VERSION_PKG.Build=$(build-id)
|
-X $VERSION_PKG.Build=$(build-id)
|
||||||
-X $VERSION_PKG.BuildDate=$(build-date)
|
-X $VERSION_PKG.BuildDate=$(build-date)
|
||||||
-X $VERSION_PKG.BuiltBy=manual
|
-X $VERSION_PKG.BuiltBy=manual
|
||||||
" -o $BINARY_OUT .
|
" -o $BINARY_OUT ./cmd/ytdl-web
|
||||||
'';
|
'';
|
||||||
scripts.clean.exec = ''
|
clean.exec = ''
|
||||||
rm -rf ./dist/ ./out/ ./tmp/
|
rm -rf ./dist/ ./out/ ./tmp/
|
||||||
go clean
|
go clean
|
||||||
'';
|
'';
|
||||||
scripts.release.exec = ''
|
release.exec = ''
|
||||||
clean
|
clean
|
||||||
goreleaser release
|
goreleaser release
|
||||||
docker-build-release
|
docker-build-release
|
||||||
'';
|
'';
|
||||||
scripts.lint.exec = "trunk check";
|
lint.exec = "trunk check";
|
||||||
scripts.fmt.exec = "trunk fmt";
|
fmt.exec = "trunk fmt";
|
||||||
|
|
||||||
scripts.docker-image.exec = "echo $DOCKER_REGISTRY/$DOCKER_ORG/$NAME";
|
docker-image.exec = "echo $DOCKER_REGISTRY/$DOCKER_ORG/$NAME";
|
||||||
|
|
||||||
scripts.docker-init.exec = ''
|
docker-init.exec = ''
|
||||||
docker buildx create \
|
docker buildx create \
|
||||||
--name $NAME \
|
--name $NAME \
|
||||||
--platform $DOCKER_PLATFORMS
|
--platform $DOCKER_PLATFORMS
|
||||||
'';
|
'';
|
||||||
|
|
||||||
scripts.docker-login.exec = "docker login $DOCKER_REGISTRY";
|
docker-login.exec = "docker login $DOCKER_REGISTRY";
|
||||||
|
|
||||||
scripts.docker-build.exec = ''
|
docker-build.exec = ''
|
||||||
platform=''${1:-linux}
|
platform=''${1:-linux}
|
||||||
output=''${2:-type=docker}
|
output=''${2:-type=docker}
|
||||||
|
|
||||||
docker-init
|
docker-init
|
||||||
PROGRESS_NO_TRUNC=1 docker buildx build . \
|
PROGRESS_NO_TRUNC=1 docker buildx build . \
|
||||||
--tag $(docker-image):$VERSION \
|
--tag $(docker-image):$VERSION \
|
||||||
--tag $(docker-image):latest \
|
--tag $(docker-image):latest \
|
||||||
--platform $platform \
|
--platform $platform \
|
||||||
--builder $NAME \
|
--builder $NAME \
|
||||||
--build-arg VERSION=$VERSION \
|
--build-arg VERSION=$VERSION \
|
||||||
--build-arg BUILD=$(build-id) \
|
--build-arg BUILD=$(build-id) \
|
||||||
--build-arg BUILD_DATE=$(build-date) \
|
--build-arg BUILD_DATE=$(build-date) \
|
||||||
--build-arg BUILT_BY="Devenv Script" \
|
--build-arg BUILT_BY="Devenv Script" \
|
||||||
--progress plain \
|
--progress plain \
|
||||||
--output $output
|
--output $output
|
||||||
'';
|
'';
|
||||||
|
|
||||||
scripts.docker-build-release.exec = "docker-build $DOCKER_PLATFORMS type=image,push=true";
|
docker-build-release.exec = "docker-build $DOCKER_PLATFORMS type=image,push=true";
|
||||||
|
};
|
||||||
|
|
||||||
enterShell = ''
|
enterShell = ''
|
||||||
echo "Welcome to the $NAME development environment."
|
echo "Welcome to the $NAME development environment."
|
||||||
|
|
@ -95,9 +105,11 @@
|
||||||
languages.go.enable = true;
|
languages.go.enable = true;
|
||||||
|
|
||||||
# https://devenv.sh/pre-commit-hooks/
|
# https://devenv.sh/pre-commit-hooks/
|
||||||
pre-commit.hooks.staticcheck.enable = true;
|
git-hooks.hooks = {
|
||||||
pre-commit.hooks.hadolint.enable = true;
|
staticcheck.enable = true;
|
||||||
pre-commit.hooks.yamllint.enable = true;
|
hadolint.enable = true;
|
||||||
|
yamllint.enable = true;
|
||||||
|
};
|
||||||
|
|
||||||
# https://devenv.sh/processes/
|
# https://devenv.sh/processes/
|
||||||
processes.web.exec = "air";
|
processes.web.exec = "air";
|
||||||
|
|
|
||||||
14
devenv.yaml
14
devenv.yaml
|
|
@ -1,5 +1,15 @@
|
||||||
|
# yaml-language-server: $schema=https://devenv.sh/devenv.schema.json
|
||||||
---
|
---
|
||||||
allowUnfree: true
|
|
||||||
inputs:
|
inputs:
|
||||||
nixpkgs:
|
nixpkgs:
|
||||||
url: github:NixOS/nixpkgs/nixpkgs-unstable
|
url: github:cachix/devenv-nixpkgs/rolling
|
||||||
|
# If you're using non-OSS software, you can set allowUnfree to true.
|
||||||
|
# allowUnfree: true
|
||||||
|
|
||||||
|
# If you're willing to use a package that's vulnerable
|
||||||
|
# permittedInsecurePackages:
|
||||||
|
# - "openssl-1.1.1w"
|
||||||
|
|
||||||
|
# If you have more than one devenv you can merge them
|
||||||
|
# imports:
|
||||||
|
# - ./backend
|
||||||
|
|
|
||||||
52
go.mod
52
go.mod
|
|
@ -1,45 +1,27 @@
|
||||||
module go.fifitido.net/ytdl-web
|
module go.fifitido.net/ytdl-web
|
||||||
|
|
||||||
go 1.20
|
go 1.25.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/adrg/xdg v0.4.0
|
github.com/a-h/templ v0.3.960
|
||||||
github.com/dgraph-io/badger/v2 v2.2007.4
|
github.com/dgraph-io/badger/v2 v2.2007.4
|
||||||
github.com/go-chi/chi/v5 v5.0.10
|
github.com/go-chi/chi/v5 v5.2.3
|
||||||
github.com/htfy96/reformism v0.0.0-20160819020323-e5bfca398e73
|
github.com/samber/slog-chi v1.17.0
|
||||||
github.com/samber/lo v1.38.1
|
|
||||||
github.com/spf13/cobra v1.7.0
|
|
||||||
github.com/spf13/viper v1.10.0
|
|
||||||
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/cespare/xxhash v1.1.0 // indirect
|
github.com/cespare/xxhash v1.1.0 // indirect
|
||||||
github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 // indirect
|
github.com/dgraph-io/ristretto v0.2.0 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da // indirect
|
||||||
github.com/fsnotify/fsnotify v1.5.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/golang/protobuf v1.5.2 // indirect
|
github.com/golang/protobuf v1.5.4 // indirect
|
||||||
github.com/golang/snappy v0.0.3 // indirect
|
github.com/golang/snappy v1.0.0 // indirect
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
github.com/klauspost/compress v1.18.1 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/klauspost/compress v1.16.3 // indirect
|
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||||
github.com/kr/text v0.2.0 // indirect
|
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||||
github.com/magiconair/properties v1.8.5 // indirect
|
golang.org/x/net v0.46.0 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.4.3 // indirect
|
golang.org/x/sys v0.38.0 // indirect
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
google.golang.org/protobuf v1.36.10 // indirect
|
||||||
github.com/pelletier/go-toml v1.9.4 // indirect
|
|
||||||
github.com/pkg/errors v0.8.1 // indirect
|
|
||||||
github.com/spf13/afero v1.6.0 // indirect
|
|
||||||
github.com/spf13/cast v1.4.1 // indirect
|
|
||||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
|
||||||
github.com/subosito/gotenv v1.2.0 // indirect
|
|
||||||
golang.org/x/net v0.8.0 // indirect
|
|
||||||
golang.org/x/sys v0.7.0 // indirect
|
|
||||||
golang.org/x/text v0.8.0 // indirect
|
|
||||||
google.golang.org/protobuf v1.27.1 // indirect
|
|
||||||
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect
|
|
||||||
gopkg.in/ini.v1 v1.66.2 // indirect
|
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
160
go.sum
160
go.sum
|
|
@ -1,185 +1,97 @@
|
||||||
cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
|
|
||||||
cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY=
|
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
||||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
|
github.com/a-h/templ v0.3.960 h1:trshEpGa8clF5cdI39iY4ZrZG8Z/QixyzEyUnA7feTM=
|
||||||
github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
|
github.com/a-h/templ v0.3.960/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
|
||||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||||
github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
|
|
||||||
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
|
||||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
|
||||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
|
||||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdwV/GHc4o=
|
github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdwV/GHc4o=
|
||||||
github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk=
|
github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk=
|
||||||
github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de h1:t0UHb5vdojIDUqktM6+xJAfScFBsVpXZmqC9dsgJmeA=
|
|
||||||
github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
|
github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
|
||||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
|
github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE=
|
||||||
|
github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU=
|
||||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
|
||||||
|
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||||
github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
|
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
|
||||||
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
|
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||||
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
|
|
||||||
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
|
||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
|
||||||
github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA=
|
|
||||||
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=
|
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
|
||||||
github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
|
||||||
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
|
||||||
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
|
||||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
|
||||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
|
|
||||||
github.com/htfy96/reformism v0.0.0-20160819020323-e5bfca398e73 h1:Shcv21tstWAyUkKxbn5bTARYej9sgEgFgTRxUPk1J8o=
|
|
||||||
github.com/htfy96/reformism v0.0.0-20160819020323-e5bfca398e73/go.mod h1:i2jduFeVras6pm8GnBWdfVKj97mGEXJogjMHzyJhukY=
|
|
||||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
|
||||||
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||||
github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY=
|
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
||||||
github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
||||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
|
||||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
|
|
||||||
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
|
|
||||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
|
||||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs=
|
|
||||||
github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
|
||||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||||
github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM=
|
|
||||||
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
|
||||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/samber/slog-chi v1.17.0 h1:zP66fV4LGF1y1Dg/+uHNY9Uxkmw1YNWaZGws6M6mPCk=
|
||||||
github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig=
|
github.com/samber/slog-chi v1.17.0/go.mod h1:a1iIuofF2gS1ii8aXIQhC6TEguLOhOvSM958fY5hToU=
|
||||||
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
|
|
||||||
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
|
|
||||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||||
github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY=
|
|
||||||
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
|
|
||||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||||
github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA=
|
|
||||||
github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
|
||||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||||
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
|
|
||||||
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
|
|
||||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
|
||||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
|
||||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
|
||||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||||
github.com/spf13/viper v1.10.0 h1:mXH0UwHS4D2HwWZa75im4xIQynLfblmWV7qcWpfv0yk=
|
|
||||||
github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
|
||||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
|
||||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||||
go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
|
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||||
go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
|
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||||
go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs=
|
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||||
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
|
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
|
||||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
|
||||||
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 h1:5llv2sWeaMSnA3w2kS57ouQQ4pudlXrR0dCgw51QK9o=
|
|
||||||
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||||
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
|
||||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||||
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
|
||||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw=
|
|
||||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
|
||||||
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
|
||||||
google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
|
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
|
||||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
|
||||||
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
|
|
||||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/ini.v1 v1.66.2 h1:XfR1dOYubytKy4Shzc2LHrrGhU0lDCfDGG1yLPmpgsI=
|
|
||||||
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
|
||||||
10
main.go
10
main.go
|
|
@ -1,10 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright © 2023 NAME HERE <EMAIL ADDRESS>
|
|
||||||
*/
|
|
||||||
package main
|
|
||||||
|
|
||||||
import "go.fifitido.net/ytdl-web/cmd"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
cmd.Execute()
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,229 @@
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
let
|
||||||
|
inherit (lib.options) mkOption mkEnableOption mkPackageOption;
|
||||||
|
inherit (lib.modules) mkIf;
|
||||||
|
inherit (lib.types) str enum int path submodule nullOr bool;
|
||||||
|
|
||||||
|
cfg = config.services.ytdl-web;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.services.ytdl-web = {
|
||||||
|
enable = mkEnableOption "ytdl-web";
|
||||||
|
package = mkPackageOption pkgs "ytdl-web" { };
|
||||||
|
|
||||||
|
user = mkOption {
|
||||||
|
type = str;
|
||||||
|
default = "ytdl-web";
|
||||||
|
description = ''
|
||||||
|
The user account ytdl-web will run under.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
group = mkOption {
|
||||||
|
type = str;
|
||||||
|
default = "ytdl-web";
|
||||||
|
description = ''
|
||||||
|
The group ytdl-web will run under.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
appEnvironment = mkOption {
|
||||||
|
type = enum [ "Development" "Staging" "Production" ];
|
||||||
|
default = "Production";
|
||||||
|
description = ''
|
||||||
|
The application environment mode.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
ytdlPackage = mkPackageOption pkgs "yt-dlp" { };
|
||||||
|
|
||||||
|
port = mkOption {
|
||||||
|
type = int;
|
||||||
|
default = 8080;
|
||||||
|
description = ''
|
||||||
|
The tcp port for the web server to listen on.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
listen = mkOption {
|
||||||
|
type = str;
|
||||||
|
default = "0.0.0.0";
|
||||||
|
example = "127.0.0.1";
|
||||||
|
description = ''
|
||||||
|
The address for the web server to listen on.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
openFirewall = mkOption {
|
||||||
|
type = bool;
|
||||||
|
default = false;
|
||||||
|
example = literalExpression "true";
|
||||||
|
description = ''
|
||||||
|
Open ports in the firewall for the ytdl-web server.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
basePath = mkOption {
|
||||||
|
type = str;
|
||||||
|
default = "/";
|
||||||
|
example = "/ytdl-web";
|
||||||
|
description = ''
|
||||||
|
The base path that the web application is hosted under.
|
||||||
|
Useful for reverse-proxies that proxy the app with a path prefix.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
cacheTTL = mkOption {
|
||||||
|
type = str;
|
||||||
|
default = "1h";
|
||||||
|
example = "2m";
|
||||||
|
description = ''
|
||||||
|
How long to keep cached metadata for.
|
||||||
|
|
||||||
|
A duration string is a possibly signed sequence of decimal numbers,
|
||||||
|
each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m".
|
||||||
|
Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
cacheDir = mkOption {
|
||||||
|
type = path;
|
||||||
|
default = "/var/cache/ytdl-web";
|
||||||
|
example = "/tmp/ytdl-web";
|
||||||
|
description = ''
|
||||||
|
The directory containing the ytdl metadata cache.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
cookies = mkOption {
|
||||||
|
type = submodule {
|
||||||
|
options = {
|
||||||
|
enable = mkEnableOption "ytdl cookies";
|
||||||
|
|
||||||
|
file = mkOption {
|
||||||
|
type = nullOr path;
|
||||||
|
default = null;
|
||||||
|
example = "/etc/ytdl-web/cookies.txt";
|
||||||
|
description = ''
|
||||||
|
The file that contains the netscape formatted cookies.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
fromBrowser = mkOption {
|
||||||
|
description = ''
|
||||||
|
Settings for obtaining cookies from a local browser's cookie storage.
|
||||||
|
'';
|
||||||
|
|
||||||
|
type = submodule {
|
||||||
|
options = {
|
||||||
|
browser = mkOption {
|
||||||
|
type = nullOr (enum [ "brave" "chrome" "chromium" "edge" "firefox" "opera" "safari" "vivaldi" ]);
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
The name of the browser to load cookies from.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
keyring = mkOption {
|
||||||
|
type = nullOr (enum [ "basictext" "gnomekeyring" "kwallet" ]);
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
The name of the keyring to use to decrypt cookies when using the chromium browser.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
profile = mkOption {
|
||||||
|
type = nullOr str;
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
The name of the browser profile to load the cookies from.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
container = mkOption {
|
||||||
|
type = nullOr str;
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
The container name to load the cookies from when using the firefox browser.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
assertions = [
|
||||||
|
{
|
||||||
|
assertion = cfg.cookies.file != null && cfg.cookies.fromBrowser.browser != null;
|
||||||
|
message = "The `services.ytdl-web.cookies.file` and `services.ytdl-web.cookies.fromBrowser.browser` options are mutually exclusive.";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = cfg.cookies.fromBrowser.browser != null &&
|
||||||
|
!(builtins.elem cfg.cookies.fromBrowser.browser [ "brave" "chrome" "chromium" "edge" "opera" "vivaldi" ]) &&
|
||||||
|
cfg.cookies.fromBrowser.keyring != null;
|
||||||
|
message = "The `services.ytdl-web.cookies.fromBrowser.keyring` only functions with a chromium-based browser.";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = cfg.cookies.fromBrowser.browser != null &&
|
||||||
|
cfg.cookies.fromBrowser.browser != "firefox" &&
|
||||||
|
cfg.cookies.fromBrowser.container != null;
|
||||||
|
message = "The `services.ytdl-web.cookies.fromBrowser.container` only functions with the firefox browser.";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
systemd.services.ytdl-web = {
|
||||||
|
description = "ytdl-web";
|
||||||
|
after = [ "networking.target" ];
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
YTDL_ENV = cfg.appEnvironment;
|
||||||
|
YTDL_BINARY_PATH = toString cfg.ytdlPackage;
|
||||||
|
YTDL_HTTP_PORT = toString cfg.port;
|
||||||
|
YTDL_HTTP_LISTEN = cfg.listen;
|
||||||
|
YTDL_HTTP_BASEPATH = cfg.basePath;
|
||||||
|
YTDL_CACHE_TTL = cfg.cacheTTL;
|
||||||
|
YTDL_CACHE_DIRPATH = cfg.cacheDir;
|
||||||
|
YTDL_COOKIES_ENABLED = if cfg.cookies.enable then "true" else "false";
|
||||||
|
YTDL_COOKIES_FILEPATH = cfg.cookies.file;
|
||||||
|
YTDL_COOKIES_FROMBROWSER_BROWSER = cfg.cookies.fromBrowser.browser;
|
||||||
|
YTDL_COOKIES_FROMBROWSER_KEYRING = cfg.cookies.fromBrowser.keyring;
|
||||||
|
YTDL_COOKIES_FROMBROWSER_PROFILE = cfg.cookies.fromBrowser.profile;
|
||||||
|
YTDL_COOKIES_FROMBROWSER_CONTAINER = cfg.cookies.fromBrowser.container;
|
||||||
|
};
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "simple";
|
||||||
|
User = cfg.user;
|
||||||
|
Group = cfg.group;
|
||||||
|
ExecStart = "${lib.getExe cfg.package} serve";
|
||||||
|
Restart = "on-failure";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d ${cfg.cacheDir} 0770 ${cfg.user} ${cfg.group} -"
|
||||||
|
];
|
||||||
|
|
||||||
|
networking.firewall = mkIf cfg.openFirewall {
|
||||||
|
allowedTCPPorts = [ cfg.port ];
|
||||||
|
};
|
||||||
|
|
||||||
|
users = {
|
||||||
|
users = mkIf (cfg.user == "ytdl-web") {
|
||||||
|
ytdl-web = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = cfg.group;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
groups = mkIf (cfg.group == "ytdl-web") {
|
||||||
|
ytdl-web = { };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
{ buildGoModule
|
||||||
|
, installShellFiles
|
||||||
|
, lib
|
||||||
|
, rev
|
||||||
|
, version
|
||||||
|
, ...
|
||||||
|
}:
|
||||||
|
buildGoModule rec {
|
||||||
|
pname = "ytdl-web";
|
||||||
|
inherit version;
|
||||||
|
|
||||||
|
src = ./..;
|
||||||
|
|
||||||
|
vendorHash = "sha256-Rqh5tGcSey53e0Ln3u5agvOwRJ6/I1eUpzRylwtjhQo=";
|
||||||
|
|
||||||
|
ldflags = [
|
||||||
|
"-s"
|
||||||
|
"-w"
|
||||||
|
"-X $VERSION_PKG.Version=${version}"
|
||||||
|
"-X $VERSION_PKG.Build=${rev}"
|
||||||
|
"-X $VERSION_PKG.BuildDate=1970-01-01T0:00:00+0000"
|
||||||
|
"-X $VERSION_PKG.BuiltBy=nix"
|
||||||
|
];
|
||||||
|
|
||||||
|
meta = with lib; {
|
||||||
|
description = "Yet another yt-dlp web frontend written in Go.";
|
||||||
|
homepage = "https://git.fifitido.net/apps/ytdl-web";
|
||||||
|
license = licenses.gpl3Only;
|
||||||
|
mainProgram = "ytdl-web";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
package components
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go.fifitido.net/ytdl-web/pkg/ytdl"
|
||||||
|
"go.fifitido.net/ytdl-web/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
templ Footer() {
|
||||||
|
<footer class="footer mt-auto py-3 bg-body-tertiary" style="font-size: 0.95rem">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row gap-2 gap-md-0">
|
||||||
|
<div class="d-flex gap-4 col-md justify-content-center">
|
||||||
|
<div class="d-flex gap-1 align-items-baseline">
|
||||||
|
Version:
|
||||||
|
<span class="text-muted">{ version.Version }</span>
|
||||||
|
<span class="text-muted text-nowrap" style="font-size: smaller">(Build: { version.Build })</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-1 align-items-baseline text-nowrap">
|
||||||
|
yt-dlp version: <span class="text-muted">{ ytdl.Default().Version() }</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2 col-md justify-content-center">
|
||||||
|
<a href="https://git.fifitido.net/apps/ytdl-web">Git Repository</a>
|
||||||
|
<span class="text-muted">© 2024 FiFiTiDo</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.960
|
||||||
|
package components
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go.fifitido.net/ytdl-web/pkg/ytdl"
|
||||||
|
"go.fifitido.net/ytdl-web/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Footer() templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<footer class=\"footer mt-auto py-3 bg-body-tertiary\" style=\"font-size: 0.95rem\"><div class=\"container\"><div class=\"row gap-2 gap-md-0\"><div class=\"d-flex gap-4 col-md justify-content-center\"><div class=\"d-flex gap-1 align-items-baseline\">Version: <span class=\"text-muted\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var2 string
|
||||||
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(version.Version)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/components/footer.templ`, Line: 15, Col: 48}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</span> <span class=\"text-muted text-nowrap\" style=\"font-size: smaller\">(Build: ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(version.Build)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/components/footer.templ`, Line: 16, Col: 93}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, ")</span></div><div class=\"d-flex gap-1 align-items-baseline text-nowrap\">yt-dlp version: <span class=\"text-muted\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(ytdl.Default().Version())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/components/footer.templ`, Line: 19, Col: 73}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</span></div></div><div class=\"d-flex gap-2 col-md justify-content-center\"><a href=\"https://git.fifitido.net/apps/ytdl-web\">Git Repository</a> <span class=\"text-muted\">© 2024 FiFiTiDo</span></div></div></div></footer>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package components
|
||||||
|
|
||||||
|
templ Navbar() {
|
||||||
|
<nav class="navbar navbar-dark bg-primary">
|
||||||
|
<div class="container">
|
||||||
|
<a href="/" class="navbar-brand">YTDL Web</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.960
|
||||||
|
package components
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
func Navbar() templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<nav class=\"navbar navbar-dark bg-primary\"><div class=\"container\"><a href=\"/\" class=\"navbar-brand\">YTDL Web</a></div></nav>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package components
|
||||||
|
|
||||||
|
import "go.fifitido.net/ytdl-web/pkg/serverctx"
|
||||||
|
|
||||||
|
script pasteClipboard() {
|
||||||
|
const pasteButton = content.querySelector("#paste-button");
|
||||||
|
const urlField = content.querySelector("#url");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const text = await navigator.clipboard.readText();
|
||||||
|
urlField.value = text;
|
||||||
|
} catch (error) {
|
||||||
|
toastr.error("Failed to paste url from clipboard.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templ PasteButton() {
|
||||||
|
if serverctx.IsHTTPS(ctx) {
|
||||||
|
<button id="paste-button" class="btn btn-outline-secondary" type="button" title="Paste" onclick={ pasteClipboard() }>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
style="width: 1.5rem; height: 1.5rem"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.960
|
||||||
|
package components
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
import "go.fifitido.net/ytdl-web/pkg/serverctx"
|
||||||
|
|
||||||
|
func pasteClipboard() templ.ComponentScript {
|
||||||
|
return templ.ComponentScript{
|
||||||
|
Name: `__templ_pasteClipboard_2c01`,
|
||||||
|
Function: `function __templ_pasteClipboard_2c01(){const pasteButton = content.querySelector("#paste-button");
|
||||||
|
const urlField = content.querySelector("#url");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const text = await navigator.clipboard.readText();
|
||||||
|
urlField.value = text;
|
||||||
|
} catch (error) {
|
||||||
|
toastr.error("Failed to paste url from clipboard.");
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
Call: templ.SafeScript(`__templ_pasteClipboard_2c01`),
|
||||||
|
CallInline: templ.SafeScriptInline(`__templ_pasteClipboard_2c01`),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PasteButton() templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
if serverctx.IsHTTPS(ctx) {
|
||||||
|
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, pasteClipboard())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<button id=\"paste-button\" class=\"btn btn-outline-secondary\" type=\"button\" title=\"Paste\" onclick=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var2 templ.ComponentScript = pasteClipboard()
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2.Call)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\"><svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" style=\"width: 1.5rem; height: 1.5rem\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184\"></path></svg></button>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getEnvKey(key string) string {
|
||||||
|
return fmt.Sprintf("YTDL_WEB_%s", strings.ReplaceAll(strings.ToUpper(key), ".", "_"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func String(key string, defaultValue string, usage string) func() string {
|
||||||
|
flagValue := flag.String(key, defaultValue, usage)
|
||||||
|
return func() string {
|
||||||
|
if flagValue != nil && *flagValue != defaultValue {
|
||||||
|
return *flagValue
|
||||||
|
}
|
||||||
|
|
||||||
|
envValue, ok := os.LookupEnv(getEnvKey(key))
|
||||||
|
if ok {
|
||||||
|
return envValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Int(key string, defaultValue int, usage string) func() int {
|
||||||
|
flagValue := flag.Int(key, defaultValue, usage)
|
||||||
|
return func() int {
|
||||||
|
if flagValue != nil && *flagValue != defaultValue {
|
||||||
|
return *flagValue
|
||||||
|
}
|
||||||
|
|
||||||
|
envValue, ok := os.LookupEnv(getEnvKey(key))
|
||||||
|
if ok {
|
||||||
|
value, err := strconv.Atoi(envValue)
|
||||||
|
if err == nil {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Bool(key string, defaultValue bool, usage string) func() bool {
|
||||||
|
flagValue := flag.Bool(key, defaultValue, usage)
|
||||||
|
return func() bool {
|
||||||
|
if flagValue != nil && *flagValue != defaultValue {
|
||||||
|
return *flagValue
|
||||||
|
}
|
||||||
|
|
||||||
|
envValue, ok := os.LookupEnv(getEnvKey(key))
|
||||||
|
if ok {
|
||||||
|
value, err := strconv.ParseBool(envValue)
|
||||||
|
if err == nil {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func StringSlice(key string, defaultValue []string, usage string) func() []string {
|
||||||
|
defaultString := strings.Join(defaultValue, ",")
|
||||||
|
flagValue := flag.String(key, defaultString, usage)
|
||||||
|
return func() []string {
|
||||||
|
if flagValue != nil && *flagValue != defaultString {
|
||||||
|
return strings.Split(*flagValue, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
envValue, ok := os.LookupEnv(getEnvKey(key))
|
||||||
|
if ok {
|
||||||
|
return strings.Split(envValue, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Duration(key string, defaultValue time.Duration, usage string) func() time.Duration {
|
||||||
|
flagValue := flag.Duration(key, defaultValue, usage)
|
||||||
|
return func() time.Duration {
|
||||||
|
if flagValue != nil && *flagValue != defaultValue {
|
||||||
|
return *flagValue
|
||||||
|
}
|
||||||
|
|
||||||
|
envValue, ok := os.LookupEnv(getEnvKey(key))
|
||||||
|
if ok {
|
||||||
|
value, err := time.ParseDuration(envValue)
|
||||||
|
if err == nil {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
120
pkg/htmx/htmx.go
120
pkg/htmx/htmx.go
|
|
@ -1,120 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
package httpx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrQueryKeyNotFound = errors.New("query key not found")
|
|
||||||
)
|
|
||||||
|
|
||||||
func Query(r *http.Request, key string) (string, error) {
|
|
||||||
values, ok := r.URL.Query()[key]
|
|
||||||
if !ok || len(values) == 0 {
|
|
||||||
return "", ErrQueryKeyNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
return values[0], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func QueryInt(r *http.Request, key string) (int, error) {
|
|
||||||
value, err := Query(r, key)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return strconv.Atoi(value)
|
|
||||||
}
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
package middlewarex
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5/middleware"
|
|
||||||
"golang.org/x/exp/slog"
|
|
||||||
)
|
|
||||||
|
|
||||||
func SlogRequestLogger(logger *slog.Logger) func(next http.Handler) http.Handler {
|
|
||||||
if logger == nil {
|
|
||||||
return func(next http.Handler) http.Handler { return next }
|
|
||||||
}
|
|
||||||
|
|
||||||
return func(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
|
|
||||||
ti := time.Now()
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
reqLogger := logger.With(
|
|
||||||
slog.String("proto", r.Proto),
|
|
||||||
slog.String("path", r.URL.Path),
|
|
||||||
slog.String("reqId", middleware.GetReqID(r.Context())),
|
|
||||||
slog.Duration("lat", time.Since(ti)),
|
|
||||||
slog.Int("status", ww.Status()),
|
|
||||||
slog.Int("size", ww.BytesWritten()),
|
|
||||||
)
|
|
||||||
|
|
||||||
reqLogger.Info("Served")
|
|
||||||
}()
|
|
||||||
|
|
||||||
next.ServeHTTP(ww, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
package server
|
|
||||||
|
|
||||||
import "github.com/go-chi/chi/v5"
|
|
||||||
|
|
||||||
type Controller interface {
|
|
||||||
Router(r chi.Router)
|
|
||||||
}
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
package server
|
|
||||||
|
|
||||||
import "golang.org/x/exp/slog"
|
|
||||||
|
|
||||||
type Options struct {
|
|
||||||
ListenAddr string
|
|
||||||
ListenPort int
|
|
||||||
Logger *slog.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func DefaultOptions() *Options {
|
|
||||||
return &Options{
|
|
||||||
ListenAddr: "127.0.0.1",
|
|
||||||
ListenPort: 8080,
|
|
||||||
Logger: slog.Default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Options) WithListenAddr(addr string) *Options {
|
|
||||||
o.ListenAddr = addr
|
|
||||||
return o
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Options) WithListenPort(port int) *Options {
|
|
||||||
o.ListenPort = port
|
|
||||||
return o
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Options) WithLogger(logger *slog.Logger) *Options {
|
|
||||||
o.Logger = logger
|
|
||||||
return o
|
|
||||||
}
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
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 *Options
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ Server = (*DefaultServer)(nil)
|
|
||||||
|
|
||||||
func New(options ...*Options) *DefaultServer {
|
|
||||||
var opts *Options
|
|
||||||
if len(options) > 0 {
|
|
||||||
opts = options[0]
|
|
||||||
} else {
|
|
||||||
opts = DefaultOptions()
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
package serverctx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type contextKey string
|
||||||
|
|
||||||
|
const (
|
||||||
|
isHTTPSKey contextKey = "isHTTPS"
|
||||||
|
basePathKey contextKey = "basePath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Middleware(basePath string) func(next http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
isHttps := r.URL.Scheme == "https" || r.Header.Get("X-Forwarded-Proto") == "https"
|
||||||
|
ctx := context.WithValue(r.Context(), isHTTPSKey, isHttps)
|
||||||
|
ctx = context.WithValue(ctx, basePathKey, basePath)
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsHTTPS(ctx context.Context) bool {
|
||||||
|
isHttps, ok := ctx.Value(isHTTPSKey).(bool)
|
||||||
|
return ok && isHttps
|
||||||
|
}
|
||||||
|
|
||||||
|
func BasePath(ctx context.Context) string {
|
||||||
|
basePath, ok := ctx.Value(basePathKey).(string)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return basePath
|
||||||
|
}
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
package utils
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/dgraph-io/badger/v2"
|
|
||||||
"golang.org/x/exp/slog"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SlogLogger struct {
|
|
||||||
logger *slog.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debugf implements badger.Logger
|
|
||||||
func (l *SlogLogger) Debugf(f string, a ...interface{}) {
|
|
||||||
l.logger.Debug(fmt.Sprintf(f, a...))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Errorf implements badger.Logger
|
|
||||||
func (l *SlogLogger) Errorf(f string, a ...interface{}) {
|
|
||||||
l.logger.Error(fmt.Sprintf(f, a...))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Infof implements badger.Logger
|
|
||||||
func (l *SlogLogger) Infof(f string, a ...interface{}) {
|
|
||||||
l.logger.Info(fmt.Sprintf(f, a...))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Warningf implements badger.Logger
|
|
||||||
func (l *SlogLogger) Warningf(f string, a ...interface{}) {
|
|
||||||
l.logger.Warn(fmt.Sprintf(f, a...))
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ badger.Logger = (*SlogLogger)(nil)
|
|
||||||
|
|
||||||
func NewBadgerLogger(logger *slog.Logger) badger.Logger {
|
|
||||||
return &SlogLogger{
|
|
||||||
logger: logger,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
package utils
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"golang.org/x/exp/slog"
|
|
||||||
)
|
|
||||||
|
|
||||||
type loggerWriter struct {
|
|
||||||
logger *slog.Logger
|
|
||||||
logLevel slog.Level
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lw *loggerWriter) Write(p []byte) (n int, err error) {
|
|
||||||
lw.logger.Log(context.Background(), lw.logLevel, string(p))
|
|
||||||
return len(p), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func LoggerWriter(logger *slog.Logger, level slog.Level) io.Writer {
|
|
||||||
return &loggerWriter{logger: logger, logLevel: level}
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
package view
|
|
||||||
|
|
||||||
import "net/http"
|
|
||||||
|
|
||||||
type Data map[string]interface{}
|
|
||||||
|
|
||||||
type Engine interface {
|
|
||||||
Load() error
|
|
||||||
Render(w http.ResponseWriter, view string, data Data, layout ...string)
|
|
||||||
}
|
|
||||||
|
|
@ -1,125 +0,0 @@
|
||||||
package html
|
|
||||||
|
|
||||||
import (
|
|
||||||
"embed"
|
|
||||||
"html/template"
|
|
||||||
"io/fs"
|
|
||||||
"net/http"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"go.fifitido.net/ytdl-web/pkg/view"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Engine struct {
|
|
||||||
fs embed.FS
|
|
||||||
tpl *template.Template
|
|
||||||
opts *Options
|
|
||||||
|
|
||||||
loadOnce sync.Once
|
|
||||||
mu sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ view.Engine = (*Engine)(nil)
|
|
||||||
|
|
||||||
func New(fs embed.FS, options ...*Options) *Engine {
|
|
||||||
var opts *Options
|
|
||||||
if len(options) > 0 && options[0] != nil {
|
|
||||||
opts = options[0]
|
|
||||||
} else {
|
|
||||||
opts = DefaultOptions()
|
|
||||||
}
|
|
||||||
|
|
||||||
tpl := template.New("/")
|
|
||||||
tpl.Delims(opts.LeftDelim, opts.RightDelim)
|
|
||||||
tpl.Funcs(opts.FuncMap)
|
|
||||||
|
|
||||||
return &Engine{
|
|
||||||
fs: fs,
|
|
||||||
tpl: tpl,
|
|
||||||
opts: opts,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Engine) Load() error {
|
|
||||||
var err error
|
|
||||||
e.loadOnce.Do(func() {
|
|
||||||
err = e.doLoad()
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Engine) doLoad() error {
|
|
||||||
e.mu.Lock()
|
|
||||||
defer e.mu.Unlock()
|
|
||||||
|
|
||||||
return fs.WalkDir(e.fs, ".", func(path string, d fs.DirEntry, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if d == nil || d.IsDir() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(path) < len(e.opts.Extension) || path[len(path)-len(e.opts.Extension):] != e.opts.Extension {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
rel, err := filepath.Rel(e.opts.BaseDir, path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
name := filepath.ToSlash(rel)
|
|
||||||
name = strings.TrimSuffix(name, e.opts.Extension)
|
|
||||||
|
|
||||||
buf, err := e.fs.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = e.tpl.New(name).Parse(string(buf))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Engine) Render(w http.ResponseWriter, view string, data view.Data, layout ...string) {
|
|
||||||
tmpl := e.tpl.Lookup(view)
|
|
||||||
if tmpl == nil {
|
|
||||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(layout) > 0 && layout[0] != "" {
|
|
||||||
lay := e.tpl.Lookup(layout[0])
|
|
||||||
if lay == nil {
|
|
||||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
e.mu.Lock()
|
|
||||||
defer e.mu.Unlock()
|
|
||||||
|
|
||||||
lay.Funcs(map[string]interface{}{
|
|
||||||
"yield": func() error {
|
|
||||||
return tmpl.Execute(w, data)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := lay.Execute(w, data); err != nil {
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tmpl.Execute(w, data); err != nil {
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
package html
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"html/template"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Options struct {
|
|
||||||
LeftDelim string
|
|
||||||
RightDelim string
|
|
||||||
Extension string
|
|
||||||
BaseDir string
|
|
||||||
FuncMap template.FuncMap
|
|
||||||
}
|
|
||||||
|
|
||||||
func DefaultOptions() *Options {
|
|
||||||
return &Options{
|
|
||||||
LeftDelim: "{{",
|
|
||||||
RightDelim: "}}",
|
|
||||||
Extension: ".html",
|
|
||||||
BaseDir: "/",
|
|
||||||
FuncMap: template.FuncMap{
|
|
||||||
"yield": func() error { return fmt.Errorf("yield called outside of a layout") },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Options) WithDelimiters(left, right string) *Options {
|
|
||||||
o.LeftDelim = left
|
|
||||||
o.RightDelim = right
|
|
||||||
return o
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Options) WithExtension(ext string) *Options {
|
|
||||||
o.Extension = ext
|
|
||||||
return o
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Options) WithBaseDir(dir string) *Options {
|
|
||||||
o.BaseDir = dir
|
|
||||||
return o
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Options) WithFunction(name string, fn interface{}) *Options {
|
|
||||||
o.FuncMap[name] = fn
|
|
||||||
return o
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Options) WithFunctions(funcs template.FuncMap) *Options {
|
|
||||||
for k, v := range funcs {
|
|
||||||
o.FuncMap[k] = v
|
|
||||||
}
|
|
||||||
return o
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
package views
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"go.fifitido.net/ytdl-web/pkg/ytdl/metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DownloadsViewModel struct {
|
||||||
|
Url string
|
||||||
|
Meta *metadata.Metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
templ videoFormatView(vm *DownloadsViewModel, vidIndex int, format metadata.Format, label string) {
|
||||||
|
{{ filename := fmt.Sprintf("%s-%s.%s", vm.Meta.ID, format.Resolution, format.Ext) }}
|
||||||
|
<div style="font-size: smaller">{ label }</div>
|
||||||
|
<div class="flex-grow-1 d-flex gap-3">
|
||||||
|
<style>
|
||||||
|
.dl-direct:after {
|
||||||
|
content: 'Download (direct)';
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<a
|
||||||
|
class="btn btn-primary flex-grow-1 dl-direct"
|
||||||
|
download={ filename }
|
||||||
|
href={ templ.SafeURL(format.Url) }
|
||||||
|
aria-label="Direct Download"
|
||||||
|
>
|
||||||
|
// Workaround for browsers that use the content of the link as the filename
|
||||||
|
<span class="d-none">{ filename }</span>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="btn btn-primary flex-grow-1"
|
||||||
|
download={ filename }
|
||||||
|
href={ pathTo(ctx, fmt.Sprintf("/download/proxy?url=%s&format=%s&index=%d", vm.Url, format.FormatID, vidIndex)) }
|
||||||
|
aria-label="Proxied Download"
|
||||||
|
>
|
||||||
|
Download (proxied)
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ videoView(vm *DownloadsViewModel, index int, video *VideoViewModel) {
|
||||||
|
if index != 0 {
|
||||||
|
<hr class="mt-5"/>
|
||||||
|
}
|
||||||
|
<div class="d-flex flex-column flex-lg-row justify-content-center gap-5 mt-5">
|
||||||
|
{{
|
||||||
|
thumbnail := video.Meta.Thumbnail
|
||||||
|
thumbnailAlt := video.Meta.ID
|
||||||
|
if video.Meta.Title != nil {
|
||||||
|
thumbnailAlt = *video.Meta.Title
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
if thumbnail != nil {
|
||||||
|
<div class="d-flex justify-content-center">
|
||||||
|
<img src={ *thumbnail } alt={ thumbnailAlt } style="max-height: 25rem; max-width: 100%; margin: 0 auto"/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class={ downloads(), "flex-lg-grow-1" }>
|
||||||
|
for _, format := range video.Formats {
|
||||||
|
@videoFormatView(vm, index, format, format.Format)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
if len(video.OtherFormats) > 0 {
|
||||||
|
{{ collapseId := fmt.Sprintf("collapse-%s-%d", vm.Meta.ID, index) }}
|
||||||
|
<div class="d-grid my-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
data-bs-target={ "#" + collapseId }
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-controls={ collapseId }
|
||||||
|
>
|
||||||
|
See More Formats
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id={ collapseId } class={ fullWidth(), "collapse" }>
|
||||||
|
<div class={ downloads(), "d-flex d-md-grid flex-column" }>
|
||||||
|
for _, format := range video.OtherFormats {
|
||||||
|
{{ label := fmt.Sprintf("ext: %s, resolution: %s, filesize: %d, note: %s", format.Ext, format.Resolution, format.Filesize, format.FormatNote) }}
|
||||||
|
@videoFormatView(vm, index, format, label)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
css downloads() {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(auto, max-content) auto;
|
||||||
|
gap: 1.5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
css fullWidth() {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
templ Downloads(vm *DownloadsViewModel) {
|
||||||
|
{{ videos := GetVideosFromMetadata(vm.Meta) }}
|
||||||
|
@Layout() {
|
||||||
|
<div class="d-flex flex-column align-items-center">
|
||||||
|
<h1>Download Video</h1>
|
||||||
|
<h2 class="fs-4 text-muted text-center">{ *vm.Meta.Title }</h2>
|
||||||
|
<p style="font-size: 0.85rem">{ vm.Url }</p>
|
||||||
|
<a
|
||||||
|
href={ pathTo(ctx) }
|
||||||
|
hx-get={ pathTo(ctx) }
|
||||||
|
hx-trigger="click"
|
||||||
|
class="btn btn-secondary btn-sm mt-3"
|
||||||
|
style="width: 30rem; max-width: 100%"
|
||||||
|
>
|
||||||
|
Download Another Video
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
for index, video := range videos {
|
||||||
|
@videoView(vm, index, video)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,463 @@
|
||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.960
|
||||||
|
package views
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"go.fifitido.net/ytdl-web/pkg/ytdl/metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DownloadsViewModel struct {
|
||||||
|
Url string
|
||||||
|
Meta *metadata.Metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
func videoFormatView(vm *DownloadsViewModel, vidIndex int, format metadata.Format, label string) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
filename := fmt.Sprintf("%s-%s.%s", vm.Meta.ID, format.Resolution, format.Ext)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div style=\"font-size: smaller\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var2 string
|
||||||
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(label)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/views/downloads.templ`, Line: 15, Col: 40}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</div><div class=\"flex-grow-1 d-flex gap-3\"><style>\n\t\t .dl-direct:after {\n\t\t content: 'Download (direct)';\n\t\t }\n\t\t</style><a class=\"btn btn-primary flex-grow-1 dl-direct\" download=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(filename)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/views/downloads.templ`, Line: 24, Col: 22}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" href=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 templ.SafeURL
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(format.Url))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/views/downloads.templ`, Line: 25, Col: 35}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" aria-label=\"Direct Download\"><span class=\"d-none\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(filename)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/views/downloads.templ`, Line: 29, Col: 34}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</span></a> <a class=\"btn btn-primary flex-grow-1\" download=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var6 string
|
||||||
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(filename)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/views/downloads.templ`, Line: 33, Col: 22}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" href=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var7 templ.SafeURL
|
||||||
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs(pathTo(ctx, fmt.Sprintf("download/proxy?url=%s&format=%s&index=%d", vm.Url, format.FormatID, vidIndex)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/views/downloads.templ`, Line: 34, Col: 113}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" aria-label=\"Proxied Download\">Download (proxied)</a></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func videoView(vm *DownloadsViewModel, index int, video *VideoViewModel) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var8 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var8 == nil {
|
||||||
|
templ_7745c5c3_Var8 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
if index != 0 {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<hr class=\"mt-5\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<div class=\"d-flex flex-column flex-lg-row justify-content-center gap-5 mt-5\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
thumbnail := video.Meta.Thumbnail
|
||||||
|
thumbnailAlt := video.Meta.ID
|
||||||
|
if video.Meta.Title != nil {
|
||||||
|
thumbnailAlt = *video.Meta.Title
|
||||||
|
}
|
||||||
|
if thumbnail != nil {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<div class=\"d-flex justify-content-center\"><img src=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var9 string
|
||||||
|
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(*thumbnail)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/views/downloads.templ`, Line: 56, Col: 25}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" alt=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var10 string
|
||||||
|
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(thumbnailAlt)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/views/downloads.templ`, Line: 56, Col: 46}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" style=\"max-height: 25rem; max-width: 100%; margin: 0 auto\"></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var11 = []any{downloads(), "flex-lg-grow-1"}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var11...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<div class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var12 string
|
||||||
|
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var11).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/views/downloads.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, format := range video.Formats {
|
||||||
|
templ_7745c5c3_Err = videoFormatView(vm, index, format, format.Format).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if len(video.OtherFormats) > 0 {
|
||||||
|
collapseId := fmt.Sprintf("collapse-%s-%d", vm.Meta.ID, index)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<div class=\"d-grid my-3\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-toggle=\"collapse\" data-bs-target=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var13 string
|
||||||
|
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs("#" + collapseId)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/views/downloads.templ`, Line: 72, Col: 37}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" aria-expanded=\"false\" aria-controls=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var14 string
|
||||||
|
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(collapseId)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/views/downloads.templ`, Line: 74, Col: 30}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\">See More Formats</button></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var15 = []any{fullWidth(), "collapse"}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var15...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<div id=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var16 string
|
||||||
|
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(collapseId)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/views/downloads.templ`, Line: 79, Col: 22}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\" class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var17 string
|
||||||
|
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var15).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/views/downloads.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var18 = []any{downloads(), "d-flex d-md-grid flex-column"}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var18...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<div class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var19 string
|
||||||
|
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var18).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/views/downloads.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, format := range video.OtherFormats {
|
||||||
|
label := fmt.Sprintf("ext: %s, resolution: %s, filesize: %d, note: %s", format.Ext, format.Resolution, format.Filesize, format.FormatNote)
|
||||||
|
templ_7745c5c3_Err = videoFormatView(vm, index, format, label).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func downloads() templ.CSSClass {
|
||||||
|
templ_7745c5c3_CSSBuilder := templruntime.GetBuilder()
|
||||||
|
templ_7745c5c3_CSSBuilder.WriteString(`display:grid;`)
|
||||||
|
templ_7745c5c3_CSSBuilder.WriteString(`grid-template-columns:minmax(auto, max-content) auto;`)
|
||||||
|
templ_7745c5c3_CSSBuilder.WriteString(`gap:1.5rem;`)
|
||||||
|
templ_7745c5c3_CSSBuilder.WriteString(`align-items:center;`)
|
||||||
|
templ_7745c5c3_CSSID := templ.CSSID(`downloads`, templ_7745c5c3_CSSBuilder.String())
|
||||||
|
return templ.ComponentCSSClass{
|
||||||
|
ID: templ_7745c5c3_CSSID,
|
||||||
|
Class: templ.SafeCSS(`.` + templ_7745c5c3_CSSID + `{` + templ_7745c5c3_CSSBuilder.String() + `}`),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fullWidth() templ.CSSClass {
|
||||||
|
templ_7745c5c3_CSSBuilder := templruntime.GetBuilder()
|
||||||
|
templ_7745c5c3_CSSBuilder.WriteString(`grid-column:span 2;`)
|
||||||
|
templ_7745c5c3_CSSID := templ.CSSID(`fullWidth`, templ_7745c5c3_CSSBuilder.String())
|
||||||
|
return templ.ComponentCSSClass{
|
||||||
|
ID: templ_7745c5c3_CSSID,
|
||||||
|
Class: templ.SafeCSS(`.` + templ_7745c5c3_CSSID + `{` + templ_7745c5c3_CSSBuilder.String() + `}`),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Downloads(vm *DownloadsViewModel) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var20 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var20 == nil {
|
||||||
|
templ_7745c5c3_Var20 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
videos := GetVideosFromMetadata(vm.Meta)
|
||||||
|
templ_7745c5c3_Var21 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<div class=\"d-flex flex-column align-items-center\"><h1>Download Video</h1><h2 class=\"fs-4 text-muted text-center\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var22 string
|
||||||
|
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(*vm.Meta.Title)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/views/downloads.templ`, Line: 106, Col: 59}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</h2><p style=\"font-size: 0.85rem\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var23 string
|
||||||
|
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(vm.Url)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/views/downloads.templ`, Line: 107, Col: 41}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</p><a href=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var24 templ.SafeURL
|
||||||
|
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinURLErrs(pathTo(ctx))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/views/downloads.templ`, Line: 109, Col: 22}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\" hx-get=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var25 string
|
||||||
|
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(pathTo(ctx))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/views/downloads.templ`, Line: 110, Col: 24}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\" hx-trigger=\"click\" class=\"btn btn-secondary btn-sm mt-3\" style=\"width: 30rem; max-width: 100%\">Download Another Video</a></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for index, video := range videos {
|
||||||
|
templ_7745c5c3_Err = videoView(vm, index, video).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
templ_7745c5c3_Err = Layout().Render(templ.WithChildren(ctx, templ_7745c5c3_Var21), templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package views
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/a-h/templ"
|
||||||
|
"go.fifitido.net/ytdl-web/pkg/serverctx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func pathTo(ctx context.Context, path ...string) templ.SafeURL {
|
||||||
|
return templ.SafeURL(strings.TrimSuffix(serverctx.BasePath(ctx), "/") + "/" + strings.TrimPrefix(strings.Join(path, "/"), "/"))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
package views
|
||||||
|
|
||||||
|
import "go.fifitido.net/ytdl-web/pkg/components"
|
||||||
|
|
||||||
|
type Error struct {
|
||||||
|
Message string
|
||||||
|
RetryUrl *string
|
||||||
|
}
|
||||||
|
|
||||||
|
templ Home(err *Error) {
|
||||||
|
@Layout() {
|
||||||
|
<h1 class="text-center">YTDL Web</h1>
|
||||||
|
<p class="text-center mb-5">
|
||||||
|
Download videos from over a thousand websites with the help of
|
||||||
|
<a href="https://github.com/yt-dlp/yt-dlp">yt-dlp</a>, a fork of youtube-dl
|
||||||
|
with more features and fixes.
|
||||||
|
<br/>
|
||||||
|
View a complete list of supported websites
|
||||||
|
<a href="https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md">here</a>.
|
||||||
|
</p>
|
||||||
|
<form hx-get={ pathTo(ctx, "/download") } hx-trigger="submit">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="url" class="form-label visually-hidden">Url</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
name="url"
|
||||||
|
id="url"
|
||||||
|
class="form-control"
|
||||||
|
required
|
||||||
|
placeholder="Enter url here then click download"
|
||||||
|
/>
|
||||||
|
@components.PasteButton()
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-grid">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
Download
|
||||||
|
<div class="spinner-border spinner-border-sm htmx-indicator ms-1" role="status">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
if err != nil {
|
||||||
|
<div class="alert alert-danger mt-4" role="alert">
|
||||||
|
<span>{ err.Message }</span>
|
||||||
|
if err.RetryUrl != nil {
|
||||||
|
<button
|
||||||
|
class="btn btn-link btn-sm pt-0 lh-base text-decoration-none"
|
||||||
|
hx-get={ pathTo(ctx, "/download") }
|
||||||
|
hx-trigger="click"
|
||||||
|
hx-target="#main-content"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-vals={ templ.JSONString(map[string]any{"url": *err.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>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.960
|
||||||
|
package views
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
import "go.fifitido.net/ytdl-web/pkg/components"
|
||||||
|
|
||||||
|
type Error struct {
|
||||||
|
Message string
|
||||||
|
RetryUrl *string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Home(err *Error) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<h1 class=\"text-center\">YTDL Web</h1><p class=\"text-center mb-5\">Download videos from over a thousand websites with the help of <a href=\"https://github.com/yt-dlp/yt-dlp\">yt-dlp</a>, a fork of youtube-dl with more features and fixes.<br>View a complete list of supported websites <a href=\"https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md\">here</a>.</p><form hx-get=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(pathTo(ctx, "/download"))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/views/home.templ`, Line: 21, Col: 41}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" hx-trigger=\"submit\"><div class=\"mb-3\"><label for=\"url\" class=\"form-label visually-hidden\">Url</label><div class=\"input-group\"><input type=\"url\" name=\"url\" id=\"url\" class=\"form-control\" required placeholder=\"Enter url here then click download\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = components.PasteButton().Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div></div><div class=\"d-grid\"><button type=\"submit\" class=\"btn btn-primary\">Download<div class=\"spinner-border spinner-border-sm htmx-indicator ms-1\" role=\"status\"><span class=\"visually-hidden\">Loading...</span></div></button></div></form>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"alert alert-danger mt-4\" role=\"alert\"><span>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(err.Message)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/views/home.templ`, Line: 47, Col: 23}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</span> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if err.RetryUrl != nil {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<button class=\"btn btn-link btn-sm pt-0 lh-base text-decoration-none\" hx-get=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(pathTo(ctx, "/download"))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/views/home.templ`, Line: 51, Col: 39}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" hx-trigger=\"click\" hx-target=\"#main-content\" hx-swap=\"innerHTML\" hx-vals=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var6 string
|
||||||
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(templ.JSONString(map[string]any{"url": *err.RetryUrl}))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/views/home.templ`, Line: 55, Col: 70}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\"><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>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
templ_7745c5c3_Err = Layout().Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
package views
|
||||||
|
|
||||||
|
import "go.fifitido.net/ytdl-web/pkg/components"
|
||||||
|
|
||||||
|
templ Layout() {
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" class="h-100">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<title>YTDL Web</title>
|
||||||
|
<link
|
||||||
|
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
|
||||||
|
rel="stylesheet"
|
||||||
|
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/toastr@2.1.4/build/toastr.min.css"
|
||||||
|
integrity="sha256-R91pD48xW+oHbpJYGn5xR0Q7tMhH4xOrWn1QqMRINtA="
|
||||||
|
crossorigin="anonymous"
|
||||||
|
/>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous" defer></script>
|
||||||
|
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.4/jquery.min.js" defer></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/toastr@2.1.4/build/toastr.min.js" integrity="sha256-Hgwq1OBpJ276HUP9H3VJkSv9ZCGRGQN+JldPJ8pNcUM=" crossorigin="anonymous" defer></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/htmx.org@1.9.4/dist/htmx.min.js" integrity="sha256-XIivRAE99i/eil5P31JNihaDSiix0V40rgmUrCfNTH4=" crossorigin="anonymous"></script>
|
||||||
|
<style>
|
||||||
|
#toast-container>div {
|
||||||
|
-moz-box-shadow: none !important;
|
||||||
|
-webkit-box-shadow: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
-ms-filter: none;
|
||||||
|
filter: none;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="d-flex flex-column h-100" data-bs-theme="dark">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
@components.Navbar()
|
||||||
|
@templ.Fragment("main-content") {
|
||||||
|
<main id="main-content" class="container my-5" hx-target="#main-content" hx-swap="outerHTML">
|
||||||
|
{ children... }
|
||||||
|
</main>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
@components.Footer()
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.960
|
||||||
|
package views
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
import "go.fifitido.net/ytdl-web/pkg/components"
|
||||||
|
|
||||||
|
func Layout() templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\" class=\"h-100\"><head><meta charset=\"UTF-8\"><meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>YTDL Web</title><link href=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css\" rel=\"stylesheet\" integrity=\"sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN\" crossorigin=\"anonymous\"><link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/toastr@2.1.4/build/toastr.min.css\" integrity=\"sha256-R91pD48xW+oHbpJYGn5xR0Q7tMhH4xOrWn1QqMRINtA=\" crossorigin=\"anonymous\"><script src=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js\" integrity=\"sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL\" crossorigin=\"anonymous\" defer></script><script src=\"https://ajax.googleapis.com/ajax/libs/jquery/3.6.4/jquery.min.js\" defer></script><script src=\"https://cdn.jsdelivr.net/npm/toastr@2.1.4/build/toastr.min.js\" integrity=\"sha256-Hgwq1OBpJ276HUP9H3VJkSv9ZCGRGQN+JldPJ8pNcUM=\" crossorigin=\"anonymous\" defer></script><script src=\"https://cdn.jsdelivr.net/npm/htmx.org@1.9.4/dist/htmx.min.js\" integrity=\"sha256-XIivRAE99i/eil5P31JNihaDSiix0V40rgmUrCfNTH4=\" crossorigin=\"anonymous\"></script><style>\n \t #toast-container>div {\n -moz-box-shadow: none !important;\n -webkit-box-shadow: none !important;\n box-shadow: none !important;\n -ms-filter: none;\n filter: none;\n opacity: 1;\n \t }\n </style></head><body class=\"d-flex flex-column h-100\" data-bs-theme=\"dark\"><div class=\"flex-shrink-0\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = components.Navbar().Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<main id=\"main-content\" class=\"container my-5\" hx-target=\"#main-content\" hx-swap=\"outerHTML\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</main>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
templ_7745c5c3_Err = templ.Fragment("main-content").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = components.Footer().Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</body></html>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
package views
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/a-h/templ"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Handler(component templ.Component) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
Render(w, r, component)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Render(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)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
package views
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go.fifitido.net/ytdl-web/pkg/ytdl/metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VideoViewModel struct {
|
||||||
|
Meta *metadata.Metadata
|
||||||
|
Formats []metadata.Format
|
||||||
|
OtherFormats []metadata.Format
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetVideosFromMetadata(meta *metadata.Metadata) []*VideoViewModel {
|
||||||
|
if meta.IsPlaylist() {
|
||||||
|
videos := make([]*VideoViewModel, 0, len(meta.Entries))
|
||||||
|
|
||||||
|
for _, entry := range meta.Entries {
|
||||||
|
videos = append(videos, GetVideosFromMetadata(&entry)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return videos
|
||||||
|
}
|
||||||
|
|
||||||
|
formats := []metadata.Format{}
|
||||||
|
otherFormats := []metadata.Format{}
|
||||||
|
|
||||||
|
for _, format := range meta.Formats {
|
||||||
|
if format.ACodec != "none" && format.VCodec != "none" && format.Protocol != "m3u8_native" {
|
||||||
|
formats = append(formats, format)
|
||||||
|
} else {
|
||||||
|
otherFormats = append(otherFormats, format)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, j := 0, len(formats)-1; i < j; i, j = i+1, j-1 {
|
||||||
|
formats[i], formats[j] = formats[j], formats[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return []*VideoViewModel{
|
||||||
|
{
|
||||||
|
Meta: meta,
|
||||||
|
Formats: formats,
|
||||||
|
OtherFormats: otherFormats,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,18 +10,21 @@ import (
|
||||||
|
|
||||||
type MetadataCache interface {
|
type MetadataCache interface {
|
||||||
Get(key string) (*metadata.Metadata, error)
|
Get(key string) (*metadata.Metadata, error)
|
||||||
Set(key string, value *metadata.Metadata, ttl time.Duration) error
|
Set(key string, value *metadata.Metadata) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type DefaultMetadataCache struct {
|
type DefaultMetadataCache struct {
|
||||||
db *badger.DB
|
db *badger.DB
|
||||||
|
ttl time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDefaultMetadataCache(db *badger.DB) *DefaultMetadataCache {
|
func NewDefaultMetadataCache(db *badger.DB, ttl time.Duration) *DefaultMetadataCache {
|
||||||
return &DefaultMetadataCache{
|
return &DefaultMetadataCache{
|
||||||
db: db,
|
db: db,
|
||||||
|
ttl: ttl,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *DefaultMetadataCache) Get(key string) (*metadata.Metadata, error) {
|
func (c *DefaultMetadataCache) Get(key string) (*metadata.Metadata, error) {
|
||||||
value := &metadata.Metadata{}
|
value := &metadata.Metadata{}
|
||||||
err := c.db.View(func(txn *badger.Txn) error {
|
err := c.db.View(func(txn *badger.Txn) error {
|
||||||
|
|
@ -37,13 +40,13 @@ func (c *DefaultMetadataCache) Get(key string) (*metadata.Metadata, error) {
|
||||||
return value, err
|
return value, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *DefaultMetadataCache) Set(key string, value *metadata.Metadata, ttl time.Duration) error {
|
func (c *DefaultMetadataCache) Set(key string, value *metadata.Metadata) error {
|
||||||
return c.db.Update(func(txn *badger.Txn) error {
|
return c.db.Update(func(txn *badger.Txn) error {
|
||||||
data, err := json.Marshal(value)
|
data, err := json.Marshal(value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
e := badger.NewEntry([]byte(key), data).WithTTL(ttl)
|
e := badger.NewEntry([]byte(key), data).WithTTL(c.ttl)
|
||||||
return txn.SetEntry(e)
|
return txn.SetEntry(e)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
package ytdl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"os/exec"
|
|
||||||
|
|
||||||
"go.fifitido.net/ytdl-web/pkg/ytdl/metadata"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Exec(binary, url string, options ...Option) error {
|
|
||||||
opts := Options{
|
|
||||||
args: []string{url},
|
|
||||||
stdin: nil,
|
|
||||||
stdout: nil,
|
|
||||||
stderr: nil,
|
|
||||||
output: nil,
|
|
||||||
}
|
|
||||||
for _, opt := range options {
|
|
||||||
if err := opt(&opts); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := exec.Command(binary, opts.args...)
|
|
||||||
|
|
||||||
if opts.stdin != nil {
|
|
||||||
cmd.Stdin = opts.stdin
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.stdout != nil {
|
|
||||||
cmd.Stdout = opts.stdout
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.stderr != nil {
|
|
||||||
cmd.Stderr = opts.stderr
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
buf, bufOk := opts.stdout.(*bytes.Buffer)
|
|
||||||
meta, metaOk := opts.output.(*metadata.Metadata)
|
|
||||||
if bufOk && metaOk {
|
|
||||||
if err := json.Unmarshal(buf.Bytes(), meta); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,109 +0,0 @@
|
||||||
package ytdl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"go.fifitido.net/ytdl-web/pkg/utils"
|
|
||||||
"go.fifitido.net/ytdl-web/pkg/ytdl/metadata"
|
|
||||||
"golang.org/x/exp/slog"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Options struct {
|
|
||||||
args []string
|
|
||||||
stdin io.Reader
|
|
||||||
stdout io.Writer
|
|
||||||
stderr io.Writer
|
|
||||||
output interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Option func(*Options) error
|
|
||||||
|
|
||||||
func WithFormat(format string) Option {
|
|
||||||
return func(opts *Options) error {
|
|
||||||
opts.args = append(opts.args, "--format", format)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithCookieFile(cookieFile string) Option {
|
|
||||||
return func(opts *Options) error {
|
|
||||||
opts.args = append(opts.args, "--cookies", cookieFile)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithDumpJson(meta *metadata.Metadata) Option {
|
|
||||||
return func(opts *Options) error {
|
|
||||||
opts.args = append(opts.args, "--dump-single-json")
|
|
||||||
opts.stdout = new(bytes.Buffer)
|
|
||||||
opts.output = meta
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithLoadJson(metadata *metadata.Metadata) Option {
|
|
||||||
return func(opts *Options) error {
|
|
||||||
opts.args = append(opts.args, "--load-info-json", "-")
|
|
||||||
|
|
||||||
json, err := json.Marshal(metadata)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
opts.stdin = bytes.NewBuffer(json)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithBrowserCookies(browser, keyring, profile, container string) Option {
|
|
||||||
return func(opts *Options) error {
|
|
||||||
var sb strings.Builder
|
|
||||||
sb.WriteString(browser)
|
|
||||||
|
|
||||||
if keyring != "" {
|
|
||||||
sb.WriteByte('+')
|
|
||||||
sb.WriteString(keyring)
|
|
||||||
}
|
|
||||||
|
|
||||||
if profile != "" {
|
|
||||||
sb.WriteByte(':')
|
|
||||||
sb.WriteString(profile)
|
|
||||||
}
|
|
||||||
|
|
||||||
if container != "" {
|
|
||||||
sb.WriteByte(':')
|
|
||||||
sb.WriteByte(':')
|
|
||||||
sb.WriteString(container)
|
|
||||||
}
|
|
||||||
|
|
||||||
opts.args = append(opts.args, "--cookies-from-browser", sb.String())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithStreamOutput(output io.Writer) Option {
|
|
||||||
return func(opts *Options) error {
|
|
||||||
opts.args = append(opts.args, "--output", "-")
|
|
||||||
opts.stdout = output
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithDebug() Option {
|
|
||||||
return func(opts *Options) error {
|
|
||||||
opts.stdout = utils.LoggerWriter(slog.With("module", "ytdl"), slog.LevelDebug)
|
|
||||||
opts.stderr = utils.LoggerWriter(slog.With("module", "ytdl"), slog.LevelDebug)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithPlaylistIndex(index int) Option {
|
|
||||||
return func(opts *Options) error {
|
|
||||||
opts.args = append(opts.args, "--playlist-items", fmt.Sprint(index+1))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package ytdl
|
||||||
|
|
||||||
|
type CookiesFromBrowserConfig struct {
|
||||||
|
Browser string
|
||||||
|
Keyring string
|
||||||
|
Profile string
|
||||||
|
Container string
|
||||||
|
}
|
||||||
|
|
||||||
|
type CookiesConfig struct {
|
||||||
|
Enabled bool
|
||||||
|
FilePath string
|
||||||
|
FromBrowser CookiesFromBrowserConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
BinaryPath string
|
||||||
|
CookiesEnabled bool
|
||||||
|
CookiesFilePath string
|
||||||
|
CookiesBrowser string
|
||||||
|
CookiesBrowserKeyring string
|
||||||
|
CookiesBrowserProfile string
|
||||||
|
CookiesBrowserContainer string
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package ytdl
|
||||||
|
|
||||||
|
type Error struct {
|
||||||
|
stdout string
|
||||||
|
stderr string
|
||||||
|
child error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Error) Error() string {
|
||||||
|
return e.child.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Error) Stdout() string {
|
||||||
|
return e.stdout
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Error) Stderr() string {
|
||||||
|
return e.stderr
|
||||||
|
}
|
||||||
174
pkg/ytdl/ytdl.go
174
pkg/ytdl/ytdl.go
|
|
@ -2,30 +2,44 @@ package ytdl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"go.fifitido.net/ytdl-web/config"
|
"log/slog"
|
||||||
|
|
||||||
"go.fifitido.net/ytdl-web/pkg/ytdl/cache"
|
"go.fifitido.net/ytdl-web/pkg/ytdl/cache"
|
||||||
"go.fifitido.net/ytdl-web/pkg/ytdl/metadata"
|
"go.fifitido.net/ytdl-web/pkg/ytdl/metadata"
|
||||||
"golang.org/x/exp/slog"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Ytdl interface {
|
type Ytdl interface {
|
||||||
GetMetadata(url string) (*metadata.Metadata, error)
|
GetMetadata(ctx context.Context, url string) (*metadata.Metadata, error)
|
||||||
Download(w io.Writer, url, format string, index int) error
|
Download(ctx context.Context, w io.Writer, url, format string, index int) error
|
||||||
Version() string
|
Version() string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ytdlImpl struct {
|
type ytdlImpl struct {
|
||||||
cfg *config.Config
|
cfg *Config
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
cache cache.MetadataCache
|
cache cache.MetadataCache
|
||||||
version string
|
version string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewYtdl(cfg *config.Config, logger *slog.Logger, cache cache.MetadataCache) Ytdl {
|
var defaultYtdl Ytdl = &ytdlImpl{}
|
||||||
|
|
||||||
|
func SetDefault(y Ytdl) {
|
||||||
|
defaultYtdl = y
|
||||||
|
}
|
||||||
|
|
||||||
|
func Default() Ytdl {
|
||||||
|
return defaultYtdl
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewYtdl(cfg *Config, logger *slog.Logger, cache cache.MetadataCache) Ytdl {
|
||||||
cmd := exec.Command(
|
cmd := exec.Command(
|
||||||
cfg.BinaryPath,
|
cfg.BinaryPath,
|
||||||
"--version",
|
"--version",
|
||||||
|
|
@ -42,32 +56,44 @@ func NewYtdl(cfg *config.Config, logger *slog.Logger, cache cache.MetadataCache)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (y *ytdlImpl) baseOptions(url string) []Option {
|
func buildBrowserCookieString(browser, keyring, profile, container string) string {
|
||||||
options := []Option{}
|
var sb strings.Builder
|
||||||
|
sb.WriteString(browser)
|
||||||
|
|
||||||
metadata, err := y.cache.Get(url)
|
if keyring != "" {
|
||||||
if err == nil {
|
sb.WriteByte('+')
|
||||||
options = append(options, WithLoadJson(metadata))
|
sb.WriteString(keyring)
|
||||||
}
|
}
|
||||||
|
|
||||||
if y.cfg.Cookies.Enabled {
|
if profile != "" {
|
||||||
if y.cfg.Cookies.FromBrowser.Browser != "" {
|
sb.WriteByte(':')
|
||||||
options = append(options, WithBrowserCookies(
|
sb.WriteString(profile)
|
||||||
y.cfg.Cookies.FromBrowser.Browser,
|
}
|
||||||
y.cfg.Cookies.FromBrowser.Keyring,
|
|
||||||
y.cfg.Cookies.FromBrowser.Profile,
|
if container != "" {
|
||||||
y.cfg.Cookies.FromBrowser.Container,
|
sb.WriteByte(':')
|
||||||
|
sb.WriteByte(':')
|
||||||
|
sb.WriteString(container)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *ytdlImpl) appendCookieArgs(args []string) []string {
|
||||||
|
if y.cfg.CookiesEnabled {
|
||||||
|
if y.cfg.CookiesBrowser != "" {
|
||||||
|
args = append(args, "--cookies-from-browser", buildBrowserCookieString(
|
||||||
|
y.cfg.CookiesBrowser,
|
||||||
|
y.cfg.CookiesBrowserKeyring,
|
||||||
|
y.cfg.CookiesBrowserProfile,
|
||||||
|
y.cfg.CookiesBrowserContainer,
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
options = append(options, WithCookieFile(y.cfg.Cookies.FilePath))
|
args = append(args, "--cookies", y.cfg.CookiesFilePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if y.cfg.IsDevelopment() {
|
return args
|
||||||
options = append(options, WithDebug())
|
|
||||||
}
|
|
||||||
|
|
||||||
return options
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (y *ytdlImpl) Version() string {
|
func (y *ytdlImpl) Version() string {
|
||||||
|
|
@ -75,24 +101,49 @@ func (y *ytdlImpl) Version() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMetadata implements Ytdl
|
// GetMetadata implements Ytdl
|
||||||
func (y *ytdlImpl) GetMetadata(url string) (*metadata.Metadata, error) {
|
func (y *ytdlImpl) GetMetadata(ctx context.Context, url string) (*metadata.Metadata, error) {
|
||||||
meta, err := y.cache.Get(url)
|
meta, err := y.cache.Get(url)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return meta, nil
|
return meta, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
meta = &metadata.Metadata{}
|
args := []string{
|
||||||
options := append(
|
url,
|
||||||
y.baseOptions(url),
|
"--dump-single-json",
|
||||||
WithDumpJson(meta),
|
}
|
||||||
)
|
|
||||||
|
|
||||||
if err := Exec(y.cfg.BinaryPath, url, options...); err != nil {
|
args = y.appendCookieArgs(args)
|
||||||
y.logger.Error("failed to get metadata", slog.String("url", url), slog.String("error", err.Error()))
|
|
||||||
|
fmt.Printf("ytdlp args: %#v\n", args)
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, y.cfg.BinaryPath, args...)
|
||||||
|
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
attrs := []any{
|
||||||
|
slog.String("url", url),
|
||||||
|
}
|
||||||
|
|
||||||
|
exiterr := &exec.ExitError{}
|
||||||
|
if errors.As(err, &exiterr) {
|
||||||
|
attrs = append(attrs, slog.Int("code", exiterr.ExitCode()))
|
||||||
|
attrs = append(attrs, slog.String("stderr", string(exiterr.Stderr)))
|
||||||
|
attrs = append(attrs, slog.String("error", exiterr.Error()))
|
||||||
|
} else {
|
||||||
|
attrs = append(attrs, slog.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
|
||||||
|
y.logger.Error("failed to get metadata", attrs...)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := y.cache.Set(url, meta, y.cfg.Cache.TTL); err != nil {
|
meta = &metadata.Metadata{}
|
||||||
|
if err := json.Unmarshal(out, meta); err != nil {
|
||||||
|
y.logger.Error("failed to unmarshal metadata", slog.String("url", url), slog.String("error", err.Error()))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := y.cache.Set(url, meta); err != nil {
|
||||||
y.logger.Warn("failed to cache metadata", slog.String("url", url), slog.String("error", err.Error()))
|
y.logger.Warn("failed to cache metadata", slog.String("url", url), slog.String("error", err.Error()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,18 +151,55 @@ func (y *ytdlImpl) GetMetadata(url string) (*metadata.Metadata, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download implements Ytdl
|
// Download implements Ytdl
|
||||||
func (y *ytdlImpl) Download(w io.Writer, url, format string, index int) error {
|
func (y *ytdlImpl) Download(ctx context.Context, w io.Writer, url, format string, index int) error {
|
||||||
options := append(
|
args := []string{
|
||||||
y.baseOptions(url),
|
url,
|
||||||
WithFormat(format),
|
"--format", format,
|
||||||
WithStreamOutput(w),
|
"--output", "-",
|
||||||
)
|
|
||||||
|
|
||||||
if index >= 0 {
|
|
||||||
options = append(options, WithPlaylistIndex(index))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := Exec(y.cfg.BinaryPath, url, options...); err != nil {
|
if index >= 0 {
|
||||||
|
args = append(args, "--playlist-ites", fmt.Sprint(index+1))
|
||||||
|
}
|
||||||
|
|
||||||
|
args = y.appendCookieArgs(args)
|
||||||
|
|
||||||
|
metadata, err := y.cache.Get(url)
|
||||||
|
if err == nil {
|
||||||
|
args = append(args, "--load-info-json", "-")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, y.cfg.BinaryPath, args...)
|
||||||
|
cmd.Stdout = w
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
json, err := json.Marshal(metadata)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Stdin = bytes.NewReader(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
exitErr := &exec.ExitError{}
|
||||||
|
if errors.As(err, &exitErr) {
|
||||||
|
if exitErr.ExitCode() == -1 {
|
||||||
|
// Handle the case where the process was terminated by a signal
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs := []any{
|
||||||
|
slog.Int("code", exitErr.ExitCode()),
|
||||||
|
slog.String("stderr", string(exitErr.Stderr)),
|
||||||
|
slog.String("error", exitErr.Error()),
|
||||||
|
}
|
||||||
|
|
||||||
|
y.logger.Error("failed to download", attrs...)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
y.logger.Error("failed to download", slog.String("url", url), slog.String("error", err.Error()))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue