Compare commits

..

38 Commits
v1.1.0 ... main

Author SHA1 Message Date
Evan Fiordeliso b871ccdd7c Update README 2024-11-07 09:03:54 -05:00
Evan Fiordeliso 59fa1bd369 Bump ytdl-web version 2024-11-06 23:27:27 -05:00
Evan Fiordeliso d814432ebb Fix incorrect thumbnail issue 2024-11-06 23:25:44 -05:00
Evan Fiordeliso 247cc211fa Fix show more formats collapse 2024-11-06 23:07:55 -05:00
Evan Fiordeliso 681225c2ae Fix config loading and fix/simplify yt-dlp command handling 2024-11-06 22:55:47 -05:00
Evan Fiordeliso 47caf17973 Bump go version and yt-dlp version in dockerfile and bump project version 2024-11-06 21:21:51 -05:00
Evan Fiordeliso 36de48cc94 Bump go version in dockerfile 2024-09-18 20:48:01 -04:00
Evan Fiordeliso 63556e2983 Bump yt-dlp version in dockerfile 2024-09-18 20:47:01 -04:00
Evan Fiordeliso 9dff066a9c Add stderr to log messages 2024-09-18 20:46:18 -04:00
Evan Fiordeliso f6d1ecd757 chore: bump yt-dlp version in docker image 2024-05-17 20:56:38 -04:00
Evan Fiordeliso aa0f47607f add nixos module to flake, add mainProgram option to package, and fix typo in readme 2024-04-16 09:59:09 -04:00
Evan Fiordeliso 77c42dffd8 rename .gitea to .forgejo 2024-04-16 00:51:59 -04:00
Evan Fiordeliso 9bba5db233 add nix flake with default package and add shell completions subcommand 2024-04-16 00:51:25 -04:00
Evan Fiordeliso c2b70e2ead Bump app version 2024-02-17 12:03:46 -05:00
Evan Fiordeliso e59915a8f5 Add collapse block with filtered out formats 2024-02-17 12:01:59 -05:00
Evan Fiordeliso 74ea23add9 Update bootstrap, format html files, remove samber/lo dep 2024-02-17 11:19:10 -05:00
Evan Fiordeliso 06ba575c42 Update devenv (including go version) 2024-02-17 11:09:31 -05:00
Evan Fiordeliso 8e4a9cb4f1 Fix proxied download by removing approx filesize 2024-02-17 11:05:49 -05:00
Evan Fiordeliso dd84590ac7 Fix ytdlp version in dockerfile 2024-01-01 15:22:58 -05:00
Evan Fiordeliso 8f9119d566 Switch from standard debian image to python image for yt-dlp 2024-01-01 15:17:21 -05:00
Evan Fiordeliso efb5aa8a33 Bump yt-dlp version 2024-01-01 15:11:09 -05:00
Evan Fiordeliso 2a51b37e81 chore: Bump version 2023-09-17 20:30:53 -04:00
Evan Fiordeliso 7cecc16695 Add extra view context to download controller 2023-09-17 20:29:27 -04:00
Evan Fiordeliso 74ea74f691 chore: bump version 2023-08-16 15:24:45 -04:00
Evan Fiordeliso 66072d125b fix: revert devenv install to nix profile install 2023-08-16 15:23:55 -04:00
Evan Fiordeliso c829fa8a37 fix: re-disable Test workflow 2023-08-16 15:22:20 -04:00
Evan Fiordeliso 94b2851984 fix: check X-Forwarded-Proto for https detection 2023-08-16 15:17:22 -04:00
Evan Fiordeliso b535db6108 Disable test workflow 2023-08-16 11:47:45 -04:00
Evan Fiordeliso a6ae38e1df chore: bump version number 2023-08-16 11:46:23 -04:00
Evan Fiordeliso 13c2054eaa chore: remove built binary 2023-08-16 11:45:45 -04:00
Evan Fiordeliso a68fcaf976 fix: version number not showing when run from docker image 2023-08-16 11:45:20 -04:00
Evan Fiordeliso 9c98c333ad fix: Hide paste button when not connected via HTTPS 2023-08-16 11:33:15 -04:00
Evan Fiordeliso ffb35c3220 fix: Attatch event listener to paste button on htmx load 2023-08-16 11:30:31 -04:00
Evan Fiordeliso 75b5c4160d Comment out cachix cache action 2023-08-15 12:54:58 -04:00
Evan Fiordeliso c4d9feb02a Fix cachix actions by adding https://github.com 2023-08-15 12:50:46 -04:00
Evan Fiordeliso a9e2006247 Add tests workflow 2023-08-15 12:48:58 -04:00
Evan Fiordeliso 8e6a2b40db Fix dockerfile and lint config.example.yaml 2023-08-14 23:11:10 -04:00
Evan Fiordeliso f0bf42dcbe Update version 2023-08-14 21:44:25 -04:00
36 changed files with 1705 additions and 552 deletions

View File

@ -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

12
.envrc
View File

@ -1,3 +1,11 @@
source_url "https://raw.githubusercontent.com/cachix/devenv/d1f7b48e35e6dee421cfd0f51481d17f77586997/direnvrc" "sha256-YBzqskFZxmNb3kYVoKD9ZixoPXJh1C9ZvTLGFRkauZ0=" if ! has nix_direnv_version || ! nix_direnv_version 2.2.1; then
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.2.1/direnvrc" "sha256-zelF0vLbEl5uaqrfIzbgNzJWGmLzCmYAkInj/LNxvKs="
fi
use devenv nix_direnv_watch_file nix/devenv.nix
nix_direnv_watch_file devenv.lock
nix_direnv_watch_file devenv.yaml
if ! use flake . --impure
then
echo "devenv could not be built. The devenv environment was not loaded. Make the necessary changes to devenv.nix and hit enter to try again." >&2
fi

View File

@ -0,0 +1,24 @@
---
name: "Test"
# yamllint disable-line rule:truthy
on:
{}
# pull_request:
# push:
jobs:
tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: https://github.com/cachix/install-nix-action@v18
# - uses: https://github.com/cachix/cachix-action@v12
# with:
# name: ytdl-web
# extraPullNames: devenv
# authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
- name: Install devenv.sh
run: nix profile install github:cachix/devenv/latest
- run: devenv shell echo ok
- run: devenv ci

7
.trunk/.gitignore vendored
View File

@ -1,7 +0,0 @@
*out
*logs
*actions
*notifications
plugins
user_trunk.yaml
user.yaml

View File

@ -1,4 +0,0 @@
# Following source doesn't work in most setups
ignored:
- SC1090
- SC1091

View File

@ -1,23 +0,0 @@
---
version: 0.1
cli:
version: 1.7.0
plugins:
sources:
- id: trunk
ref: v0.0.15
uri: https://github.com/trunk-io/plugins
lint:
enabled:
- git-diff-check
- gitleaks@8.16.2
- gofmt@1.19.3
- golangci-lint@1.52.2
- hadolint@2.12.0
- prettier@2.8.7
- taplo@0.7.0
runtimes:
enabled:
- go@1.19.5
- node@18.12.1
- python@3.10.8

View File

@ -1,8 +1,9 @@
ARG GOLANG_VERSION="1.20.5" ARG GOLANG_VERSION="1.22.1"
ARG ALPINE_VERSION="3.18" ARG DEBIAN_VERSION="bookworm"
FROM golang:${GOLANG_VERSION}-alpine${ALPINE_VERSION} AS build FROM golang:${GOLANG_VERSION}-${DEBIAN_VERSION} AS build
ENV VERSION_PKG="go.fifitido.net/ytdl-web/version"
ARG VERSION=latest ARG VERSION=latest
ARG BUILD="unknown" ARG BUILD="unknown"
ARG BUILD_DATE="unknown" ARG BUILD_DATE="unknown"
@ -17,18 +18,25 @@ RUN go mod download
COPY . . COPY . .
RUN go build \ RUN go build \
-ldflags="-X $VERSION_PKG.Version=${VERSION}" \ -ldflags=" \
-ldflags="-X $VERSION_PKG.Build=${BUILD}" \ -X $VERSION_PKG.Version=$VERSION \
-ldflags="-X $VERSION_PKG.BuildDate=${BUILD_DATE}" \ -X $VERSION_PKG.Build=$BUILD \
-ldflags="-X $VERSION_PKG.BuiltBy=${BUILT_BY}" \ -X $VERSION_PKG.BuildDate=$BUILD_DATE \
-o /ytdl-web . -X \"$VERSION_PKG.BuiltBy=$BUILT_BY\" \
" -o /ytdl-web .
FROM alpine:${ALPINE_VERSION} FROM python:${DEBIAN_VERSION}
ARG YTDLP_VERSION="2024.11.04"
WORKDIR /app WORKDIR /app
ARG YTDLP_VERSION="2023.07.06-r0" # hadolint ignore=DL3008
RUN apk add --no-cache yt-dlp==${YTDLP_VERSION} RUN apt-get update \
&& apt-get install --no-install-recommends -y wget \
&& wget --progress=dot:giga "https://github.com/yt-dlp/yt-dlp/releases/download/${YTDLP_VERSION}/yt-dlp" \
&& install -pm755 yt-dlp /usr/bin/yt-dlp \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
COPY --from=build /ytdl-web ./ COPY --from=build /ytdl-web ./
@ -39,4 +47,4 @@ ENV YTDL_HTTP_PORT=8080 \
YTDL_BINARYPATH=/usr/bin/yt-dlp \ YTDL_BINARYPATH=/usr/bin/yt-dlp \
YTDL_CONFIGDIR=/config YTDL_CONFIGDIR=/config
ENTRYPOINT [ "./ytdl-web" ] ENTRYPOINT [ "./ytdl-web" ]

View File

@ -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

View File

@ -2,11 +2,11 @@ package controllers
import ( import (
"fmt" "fmt"
"io"
"net/http" "net/http"
"net/url" "net/url"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/samber/lo"
"github.com/spf13/viper" "github.com/spf13/viper"
"go.fifitido.net/ytdl-web/app" "go.fifitido.net/ytdl-web/app"
"go.fifitido.net/ytdl-web/app/models" "go.fifitido.net/ytdl-web/app/models"
@ -16,6 +16,7 @@ import (
"go.fifitido.net/ytdl-web/pkg/view" "go.fifitido.net/ytdl-web/pkg/view"
"go.fifitido.net/ytdl-web/pkg/ytdl" "go.fifitido.net/ytdl-web/pkg/ytdl"
"go.fifitido.net/ytdl-web/pkg/ytdl/metadata" "go.fifitido.net/ytdl-web/pkg/ytdl/metadata"
"go.fifitido.net/ytdl-web/version"
"golang.org/x/exp/slog" "golang.org/x/exp/slog"
) )
@ -48,20 +49,26 @@ func (c *DownloadController) getUrlParam(r *http.Request) (string, bool) {
return "", false return "", false
} }
return string(urlBytes), true return urlBytes, true
} }
func (c *DownloadController) ListDownloadLinks(w http.ResponseWriter, r *http.Request) { func (c *DownloadController) ListDownloadLinks(w http.ResponseWriter, r *http.Request) {
hx := htmx.New(w, r) hx := htmx.New(w, r)
layout := []string{} var layout []string
if !hx.IsHtmxRequest() { if !hx.IsHtmxRequest() {
layout = append(layout, "layouts/main") layout = append(layout, "layouts/main")
} }
isSecure := r.URL.Scheme == "https" || r.Header.Get("X-Forwarded-Proto") == "https"
videoUrl, ok := c.getUrlParam(r) videoUrl, ok := c.getUrlParam(r)
if !ok { if !ok {
app.Views.Render(w, "index", view.Data{ app.Views.Render(w, "index", view.Data{
"BasePath": viper.GetString("base_path"), "BasePath": viper.GetString("base_path"),
"Version": version.Version,
"Build": version.Build,
"BinaryVersion": c.ytdl.Version(),
"IsSecure": isSecure,
"Error": view.Data{ "Error": view.Data{
"Message": "Invalid URL", "Message": "Invalid URL",
}, },
@ -72,7 +79,11 @@ func (c *DownloadController) ListDownloadLinks(w http.ResponseWriter, r *http.Re
meta, err := c.ytdl.GetMetadata(videoUrl) meta, err := c.ytdl.GetMetadata(videoUrl)
if err != nil { if err != nil {
app.Views.Render(w, "index", view.Data{ app.Views.Render(w, "index", view.Data{
"BasePath": viper.GetString("base_path"), "BasePath": viper.GetString("base_path"),
"Version": version.Version,
"Build": version.Build,
"BinaryVersion": c.ytdl.Version(),
"IsSecure": isSecure,
"Error": view.Data{ "Error": view.Data{
"Message": "Could not find a video at that url", "Message": "Could not find a video at that url",
"RetryUrl": videoUrl, "RetryUrl": videoUrl,
@ -86,13 +97,21 @@ func (c *DownloadController) ListDownloadLinks(w http.ResponseWriter, r *http.Re
} }
app.Views.Render(w, "download", view.Data{ app.Views.Render(w, "download", view.Data{
"BasePath": viper.GetString("base_path"), "BasePath": viper.GetString("base_path"),
"Url": videoUrl, "Version": version.Version,
"Meta": meta, "Build": version.Build,
"Videos": models.GetVideosFromMetadata(meta), "BinaryVersion": c.ytdl.Version(),
"IsSecure": isSecure,
"Url": videoUrl,
"Meta": meta,
"Videos": models.GetVideosFromMetadata(meta),
}, layout...) }, layout...)
} }
var (
BUF_LEN = 1024
)
func (c *DownloadController) ProxyDownload(w http.ResponseWriter, r *http.Request) { func (c *DownloadController) ProxyDownload(w http.ResponseWriter, r *http.Request) {
videoUrl, ok := c.getUrlParam(r) videoUrl, ok := c.getUrlParam(r)
if !ok { if !ok {
@ -121,10 +140,15 @@ func (c *DownloadController) ProxyDownload(w http.ResponseWriter, r *http.Reques
} }
video := videos[index] video := videos[index]
format, ok := lo.Find(video.Formats, func(format metadata.Format) bool {
return format.FormatID == formatId var format *metadata.Format
}) for _, f := range video.Formats {
if !ok { if f.FormatID == formatId {
format = &f
break
}
}
if format == nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return return
} }
@ -132,17 +156,25 @@ func (c *DownloadController) ProxyDownload(w http.ResponseWriter, r *http.Reques
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s-%s.%s\"", meta.ID, format.Resolution, format.Ext)) w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s-%s.%s\"", meta.ID, format.Resolution, format.Ext))
if format.Filesize != nil { if format.Filesize != nil {
w.Header().Set("Content-Length", fmt.Sprint(*format.Filesize)) 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 { if len(videos) == 1 {
index = -1 index = -1
} }
if err := c.ytdl.Download(w, videoUrl, format.FormatID, index); err != nil { read, write := io.Pipe()
go func() {
_, err := io.Copy(w, read)
if err != nil {
slog.Error("Failed to copy", slog.String("error", err.Error()))
}
}()
if err := c.ytdl.Download(write, videoUrl, format.FormatID, index); err != nil {
slog.Error("Failed to download", slog.String("error", err.Error())) slog.Error("Failed to download", slog.String("error", err.Error()))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
} }
write.Close()
} }

View File

@ -31,6 +31,7 @@ func (c *HomeController) Router(r chi.Router) {
func (c *HomeController) Index(w http.ResponseWriter, r *http.Request) { func (c *HomeController) Index(w http.ResponseWriter, r *http.Request) {
hx := htmx.New(w, r) hx := htmx.New(w, r)
isSecure := r.URL.Scheme == "https" || r.Header.Get("X-Forwarded-Proto") == "https"
if hx.IsHtmxRequest() { if hx.IsHtmxRequest() {
hx.PushUrl("/") hx.PushUrl("/")
@ -40,6 +41,7 @@ func (c *HomeController) Index(w http.ResponseWriter, r *http.Request) {
"Version": version.Version, "Version": version.Version,
"Build": version.Build, "Build": version.Build,
"BinaryVersion": c.ytdl.Version(), "BinaryVersion": c.ytdl.Version(),
"IsSecure": isSecure,
}) })
} else { } else {
app.Views.Render(w, "index", view.Data{ app.Views.Render(w, "index", view.Data{
@ -47,6 +49,7 @@ func (c *HomeController) Index(w http.ResponseWriter, r *http.Request) {
"Version": version.Version, "Version": version.Version,
"Build": version.Build, "Build": version.Build,
"BinaryVersion": c.ytdl.Version(), "BinaryVersion": c.ytdl.Version(),
"IsSecure": isSecure,
}, "layouts/main") }, "layouts/main")
} }
} }

View File

@ -1,25 +1,36 @@
package models package models
import ( import (
"github.com/samber/lo"
"go.fifitido.net/ytdl-web/pkg/ytdl/metadata" "go.fifitido.net/ytdl-web/pkg/ytdl/metadata"
) )
type Video struct { type Video struct {
Meta *metadata.Metadata Meta *metadata.Metadata
Formats []metadata.Format Formats []metadata.Format
OtherFormats []metadata.Format
} }
func GetVideosFromMetadata(meta *metadata.Metadata) []Video { func GetVideosFromMetadata(meta *metadata.Metadata) []Video {
if meta.IsPlaylist() { if meta.IsPlaylist() {
return lo.Map(meta.Entries, func(video metadata.Metadata, _ int) Video { videos := make([]Video, 0, len(meta.Entries))
return GetVideosFromMetadata(&video)[0]
}) for _, entry := range meta.Entries {
videos = append(videos, GetVideosFromMetadata(&entry)...)
}
return videos
} }
formats := lo.Filter(meta.Formats, func(item metadata.Format, _ int) bool { formats := []metadata.Format{}
return item.ACodec != "none" && item.VCodec != "none" && item.Protocol != "m3u8_native" 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 { for i, j := 0, len(formats)-1; i < j; i, j = i+1, j-1 {
formats[i], formats[j] = formats[j], formats[i] formats[i], formats[j] = formats[j], formats[i]
@ -27,8 +38,9 @@ func GetVideosFromMetadata(meta *metadata.Metadata) []Video {
return []Video{ return []Video{
{ {
Meta: meta, Meta: meta,
Formats: formats, Formats: formats,
OtherFormats: otherFormats,
}, },
} }
} }

View File

@ -3,6 +3,7 @@ package app
import ( import (
"embed" "embed"
"encoding/json" "encoding/json"
"fmt"
"html/template" "html/template"
"net/url" "net/url"
@ -39,6 +40,25 @@ var Views = html.New(
"Format": format, "Format": format,
} }
}). }).
WithFunction("sprintf", func(format string, args ...any) string {
return fmt.Sprintf(format, args...)
}).
WithFunction("filesize", func(size *int) string {
if size == nil {
return "unknown"
}
const unit = 1000
if *size < unit {
return fmt.Sprintf("%d B", *size)
}
div, exp := int64(unit), 0
for n := *size / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB",
float64(*size)/float64(div), "kMGTPE"[exp])
}).
WithFunctions(reformism.FuncsHTML), WithFunctions(reformism.FuncsHTML),
) )

View File

@ -2,51 +2,62 @@
<h1>Download Video</h1> <h1>Download Video</h1>
<h2 class="fs-4 text-muted text-center">{{.Meta.Title}}</h2> <h2 class="fs-4 text-muted text-center">{{.Meta.Title}}</h2>
<p style="font-size: 0.85rem">{{.Url}}</p> <p style="font-size: 0.85rem">{{.Url}}</p>
<a <a href="{{.BasePath}}/" hx-get="{{.BasePath}}/" hx-trigger="click" hx-target="#main-content"
href="{{.BasePath}}/" class="btn btn-secondary btn-sm mt-3" style="width: 30rem; max-width: 100%">
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 Download Another Video
</a> </a>
</div> </div>
{{$root := .}} {{range $vidIndex, $video := .Videos}} {{if not (eq $vidIndex {{$root := .}}
0)}} {{range $vidIndex, $video := .Videos}}
{{if not (eq $vidIndex 0)}}
<hr class="mt-5" /> <hr class="mt-5" />
{{end}} {{end}}
<div class="d-flex flex-column flex-lg-row justify-content-center gap-5 mt-5"> <div class="d-flex flex-column flex-lg-row justify-content-center gap-5 mt-5">
<div class="d-flex justify-content-center"> <div class="d-flex justify-content-center">
<img <img src="{{$video.Meta.Thumbnail}}" alt="{{$video.Meta.Title}}" style="max-height: 25rem; max-width: 100%; margin: 0 auto" />
src="{{.Meta.Thumbnail}}"
alt="{{.Meta.Title}}"
style="max-height: 25rem; max-width: 100%; margin: 0 auto"
/>
</div> </div>
<div class="downloads flex-lg-grow-1"> <div class="downloads flex-lg-grow-1">
{{range $index, $format := $video.Formats}} {{range $index, $format := $video.Formats}}
<div style="font-size: smaller">{{$format.Format}}</div> {{template "format" (map "root" $root "format" $format "label" $format.Format "vidIndex" $vidIndex)}}
<div class="flex-grow-1 d-flex gap-3"> {{end}}
<a </div>
class="btn btn-primary flex-grow-1" </div>
download="{{$root.Meta.ID}}-{{$format.Resolution}}.{{$format.Ext}}" {{if gt (len $video.OtherFormats) 0}}
P <div class="d-grid my-3">
href="{{$format.Url}}" <button
> type="button"
Download (direct) class="btn btn-secondary"
</a> data-bs-toggle="collapse"
<a data-bs-target="#collapse-{{$root.Meta.ID}}-{{$vidIndex}}"
class="btn btn-primary flex-grow-1" aria-expanded="false"
download="{{$root.Meta.ID}}-{{$format.Resolution}}.{{$format.Ext}}" aria-controls="collapse-{{$root.Meta.ID}}-{{$vidIndex}}"
href="{{$root.BasePath}}/download/proxy?url={{queryEscape $root.Url}}&format={{$format.FormatID}}&index={{$vidIndex}}" >
> See More Formats
Download (proxied) </button>
</a> </div>
</div> <div id="collapse-{{$root.Meta.ID}}-{{$vidIndex}}" class="collapse">
<div class="downloads d-flex d-md-grid flex-column">
{{range $index, $format := $video.OtherFormats}}
{{$label := sprintf "ext: %s, resolution: %s, filesize: %s, note: %s" $format.Ext $format.Resolution
(filesize $format.Filesize) $format.FormatNote}}
{{template "format" (map "root" $root "format" $format "label" $label "vidIndex" $vidIndex)}}
{{end}} {{end}}
</div> </div>
</div> </div>
{{end}} {{end}}
{{end}}
{{define "format"}}
<div style="font-size: smaller">{{.label}}</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}}

View File

@ -5,58 +5,30 @@
with more features and fixes. with more features and fixes.
<br /> <br />
View a complete list of supported websites View a complete list of supported websites
<a href="https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md" <a href="https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md">here</a>.
>here</a
>.
</p> </p>
<form <form hx-get="{{.BasePath}}/download" hx-trigger="submit" hx-target="#main-content" hx-swap="innerHTML">
hx-get="{{.BasePath}}/download"
hx-trigger="submit"
hx-target="#main-content"
hx-swap="innerHTML"
>
<div class="mb-3"> <div class="mb-3">
<label for="url" class="form-label visually-hidden">Url</label> <label for="url" class="form-label visually-hidden">Url</label>
<div class="input-group"> <div class="input-group">
<input <input type="url" name="url" id="url" class="form-control" required
type="url" placeholder="Enter url here then click download" />
name="url" {{if .IsSecure}}
id="url" <button id="paste-button" class="btn btn-outline-secondary" type="button" title="Paste">
class="form-control" <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
required style="width: 1.5rem; height: 1.5rem">
placeholder="Enter url here then click download" <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" />
<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> </svg>
</button> </button>
{{end}}
</div> </div>
</div> </div>
<div class="d-grid"> <div class="d-grid">
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary">
Download Download
<div <div class="spinner-border spinner-border-sm htmx-indicator ms-1" role="status">
class="spinner-border spinner-border-sm htmx-indicator ms-1"
role="status"
>
<span class="visually-hidden">Loading...</span> <span class="visually-hidden">Loading...</span>
</div> </div>
</button> </button>
@ -68,22 +40,13 @@
<span>{{.Error.Message}}</span> <span>{{.Error.Message}}</span>
{{if .Error.RetryUrl}} {{if .Error.RetryUrl}}
<button <button class="btn btn-link btn-sm pt-0 lh-base text-decoration-none" hx-get="/download" hx-trigger="click"
class="btn btn-link btn-sm pt-0 lh-base text-decoration-none" hx-target="#main-content" hx-swap="innerHTML" hx-vals='{"url": "{{.Error.RetryUrl}}"}'>
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> <span class="text-decoration-underline">Try Again</span>
<div <div class="spinner-border spinner-border-sm htmx-indicator ms-1" role="status">
class="spinner-border spinner-border-sm htmx-indicator ms-1"
role="status"
>
<span class="visually-hidden">Loading...</span> <span class="visually-hidden">Loading...</span>
</div> </div>
</button> </button>
{{end}} {{end}}
</div> </div>
{{end}} {{end}}

View File

@ -1,84 +1,75 @@
<!DOCTYPE html> <!doctype html>
<html lang="en" class="h-100"> <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 { <head>
display: grid; <meta charset="UTF-8" />
grid-template-columns: minmax(auto, max-content) auto; <meta http-equiv="X-UA-Compatible" content="IE=edge" />
gap: 1.5rem; <meta name="viewport" content="width=device-width, initial-scale=1.0" />
align-items: center; <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;
}
.see-more-btn, .downloads {
.collapse { display: grid;
grid-column: span 2; grid-template-columns: minmax(auto, max-content) auto;
} gap: 1.5rem;
</style> align-items: center;
</head> }
<body class="d-flex flex-column h-100" data-bs-theme="dark"> .see-more-btn,
<div class="flex-shrink-0"> .collapse {
{{template "partials/navbar" .}} grid-column: span 2;
<main id="main-content" class="container my-5">{{yield}}</main> }
</div> </style>
{{template "partials/footer" .}} </head>
<script>
const pasteButton = document.getElementById("paste-button"); <body class="d-flex flex-column h-100" data-bs-theme="dark">
const urlField = document.getElementById("url"); <div class="flex-shrink-0">
pasteButton.addEventListener("click", async () => { {{template "partials/navbar" .}}
try { <main id="main-content" class="container my-5">{{yield}}</main>
const text = await navigator.clipboard.readText(); </div>
urlField.value = text; {{template "partials/footer" .}}
} catch (error) { <script>
toastr.error("Failed to paste url from clipboard."); /**
} * @param content {Element}
}); */
</script> function setupPaste(content) {
</body> const pasteButton = content.querySelector("#paste-button");
</html> 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>

View File

@ -5,9 +5,7 @@
<div class="d-flex gap-1 align-items-baseline"> <div class="d-flex gap-1 align-items-baseline">
Version: Version:
<span class="text-muted">{{.Version}}</span> <span class="text-muted">{{.Version}}</span>
<span class="text-muted text-nowrap" style="font-size: smaller" <span class="text-muted text-nowrap" style="font-size: smaller">(Build: {{.Build}})</span>
>(Build: {{.Build}})</span
>
</div> </div>
<div class="d-flex gap-1 align-items-baseline text-nowrap"> <div class="d-flex gap-1 align-items-baseline text-nowrap">
yt-dlp version: <span class="text-muted">{{.BinaryVersion}}</span> yt-dlp version: <span class="text-muted">{{.BinaryVersion}}</span>
@ -15,8 +13,8 @@
</div> </div>
<div class="d-flex gap-2 col-md justify-content-center"> <div class="d-flex gap-2 col-md justify-content-center">
<a href="https://git.fifitido.net/apps/ytdl-web">Git Repository</a> <a href="https://git.fifitido.net/apps/ytdl-web">Git Repository</a>
<span class="text-muted">&copy; 2023 FiFiTiDo</span> <span class="text-muted">&copy; 2024 FiFiTiDo</span>
</div> </div>
</div> </div>
</div> </div>
</footer> </footer>

View File

@ -2,4 +2,4 @@
<div class="container"> <div class="container">
<a href="/" class="navbar-brand">YTDL Web</a> <a href="/" class="navbar-brand">YTDL Web</a>
</div> </div>
</nav> </nav>

85
cmd/completion.go Normal file
View File

@ -0,0 +1,85 @@
/*
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
// completionCmd represents the completion command
var completionCmd = &cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate completion script",
Long: fmt.Sprintf(`To load completions:
Bash:
$ source <(%[1]s completion bash)
# To load completions for each session, execute once:
# Linux:
$ %[1]s completion bash > /etc/bash_completion.d/%[1]s
# macOS:
$ %[1]s completion bash > $(brew --prefix)/etc/bash_completion.d/%[1]s
Zsh:
# If shell completion is not already enabled in your environment,
# you will need to enable it. You can execute the following once:
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
# To load completions for each session, execute once:
$ %[1]s completion zsh > "${fpath[1]}/_%[1]s"
# You will need to start a new shell for this setup to take effect.
fish:
$ %[1]s completion fish | source
# To load completions for each session, execute once:
$ %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish
PowerShell:
PS> %[1]s completion powershell | Out-String | Invoke-Expression
# To load completions for every new session, run:
PS> %[1]s completion powershell > %[1]s.ps1
# and source this file from your PowerShell profile.
`, rootCmd.Root().Name()),
DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
Run: func(cmd *cobra.Command, args []string) {
switch args[0] {
case "bash":
cmd.Root().GenBashCompletion(os.Stdout)
case "zsh":
cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
cmd.Root().GenFishCompletion(os.Stdout, true)
case "powershell":
cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
}
},
}
func init() {
rootCmd.AddCommand(completionCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// completionCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// completionCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

View File

@ -1,20 +1,16 @@
/* /*
Copyright © 2023 Evan Fiordeliso <evan.fiordeliso@gmail.com> Copyright © 2024 Evan Fiordeliso <evan.fiordeliso@gmail.com>
*/ */
package cmd package cmd
import ( import (
"errors"
"fmt"
"os" "os"
"github.com/dgraph-io/badger/v2"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
"go.fifitido.net/ytdl-web/app/controllers"
"go.fifitido.net/ytdl-web/config" "go.fifitido.net/ytdl-web/config"
"go.fifitido.net/ytdl-web/pkg/server"
"go.fifitido.net/ytdl-web/pkg/utils"
"go.fifitido.net/ytdl-web/pkg/ytdl"
"go.fifitido.net/ytdl-web/pkg/ytdl/cache"
"golang.org/x/exp/slog" "golang.org/x/exp/slog"
) )
@ -25,47 +21,13 @@ var (
rootCmd = &cobra.Command{ rootCmd = &cobra.Command{
Use: "ytdl-web", Use: "ytdl-web",
Short: "A web frontend for yt-dlp", Short: "A web frontend for yt-dlp",
Long: `YTDL Web Long: `YTDL Web is a web application that grabs the links to videos
from over a thousand websites using the yt-dlp project under the hood.`,
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() { func Execute() error {
err := rootCmd.Execute() return rootCmd.Execute()
if err != nil {
os.Exit(1)
}
} }
func init() { func init() {
@ -98,14 +60,16 @@ func initConfig() {
cfg, err = config.LoadConfig() cfg, err = config.LoadConfig()
} }
if err != nil { notFound := &viper.ConfigFileNotFoundError{}
slog.Error("Error loading configuration", slog.String("error", err.Error())) switch {
os.Exit(1) case err != nil && !errors.As(err, notFound):
cobra.CheckErr(err)
case err != nil && errors.As(err, notFound):
// The config file is optional, we shouldn't exit when the config is not found
break
default:
fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
} }
initLogging()
slog.Info("Configuration loaded")
} }
func initLogging() { func initLogging() {

68
cmd/serve.go Normal file
View File

@ -0,0 +1,68 @@
/*
Copyright © 2024 Evan Fiordeliso <evan.fiordeliso@gmail.com>
*/
package cmd
import (
"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/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 (
serveCmd = &cobra.Command{
Use: "serve",
Short: "Serve the ytdl-web application",
Long: `Serve the ytdl-web application`,
RunE: func(cmd *cobra.Command, args []string) error {
initLogging()
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 init() {
rootCmd.AddCommand(serveCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// completionCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// completionCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

View File

@ -1,9 +1,11 @@
---
# The server environment # The server environment
# For dev environments use Development # For dev environments use Development
# For prod environments use Production # For prod environments use Production
# For staging envronments use Staging # For staging envronments use Staging
env: Production env: Production
# The path to the yt-dlp binary, if it is already in your $PATH just yt-dlp will work. # The path to the yt-dlp binary
# If it is already in your $PATH just yt-dlp will work.
binaryPath: yt-dlp binaryPath: yt-dlp
http: http:
# The port to listen on # The port to listen on
@ -29,7 +31,8 @@ cookies:
# Settings for using cookies from a browser's cookies store # Settings for using cookies from a browser's cookies store
fromBrowser: fromBrowser:
# The name of the browser to load cookies from. # The name of the browser to load cookies from.
# Currently supported browsers are: brave, chrome, chromium, edge, firefox, opera, safari, vivaldi. # Currently supported browsers are: brave, chrome, chromium, edge,
# firefox, opera, safari, vivaldi.
browser: firefox browser: firefox
# The keyring used for decrypting Chromium cookies on Linux # The keyring used for decrypting Chromium cookies on Linux

View File

@ -2,6 +2,7 @@ package config
import ( import (
"os" "os"
"path"
"strings" "strings"
"time" "time"
@ -10,11 +11,11 @@ import (
) )
type Config struct { type Config struct {
Env string `mapstructure:"env"` Env string `mapstructure:"env"`
BinaryPath string `mapstructure:"binaryPath"` Ytdlp ConfigYtdlp `mapstructure:"ytdlp"`
HTTP ConfigHTTP `mapstructure:"http"` HTTP ConfigHTTP `mapstructure:"http"`
Cache ConfigCache `mapstructure:"cache"` Cache ConfigCache `mapstructure:"cache"`
Cookies ConfigCookies `mapstructure:"cookies"` Cookies ConfigCookies `mapstructure:"cookies"`
} }
func (c *Config) IsProduction() bool { func (c *Config) IsProduction() bool {
@ -29,6 +30,10 @@ func (c *Config) IsStaging() bool {
return c.Env == "Staging" return c.Env == "Staging"
} }
type ConfigYtdlp struct {
BinaryPath string `mapstructure:"binaryPath"`
}
type ConfigHTTP struct { type ConfigHTTP struct {
Port int `mapstructure:"port"` Port int `mapstructure:"port"`
Listen string `mapstructure:"listen"` Listen string `mapstructure:"listen"`
@ -56,8 +61,8 @@ type ConfigCookiesFromBrowser struct {
func DefaultConfig() *Config { func DefaultConfig() *Config {
return &Config{ return &Config{
Env: "Production", Env: "Production",
BinaryPath: "yt-dlp", Ytdlp: ConfigYtdlp{BinaryPath: "yt-dlp"},
HTTP: ConfigHTTP{ HTTP: ConfigHTTP{
Port: 8080, Port: 8080,
Listen: "127.0.0.1", Listen: "127.0.0.1",
@ -76,46 +81,44 @@ func DefaultConfig() *Config {
} }
func LoadConfig(paths ...string) (*Config, error) { func LoadConfig(paths ...string) (*Config, error) {
v := viper.New() viper.SetEnvPrefix("YTDL")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()
v.SetEnvPrefix("YTDL") viper.SetConfigName("config")
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) viper.SetConfigType("yaml")
v.AutomaticEnv()
v.SetConfigName("config")
v.SetConfigType("yaml")
if len(paths) > 0 { if len(paths) > 0 {
for _, path := range paths { for _, p := range paths {
v.AddConfigPath(path) viper.AddConfigPath(path.Dir(p))
} }
} else { } else {
envDir := os.Getenv("YTDL_CONFIGDIR") envDir := os.Getenv("YTDL_CONFIGDIR")
if envDir != "" { if envDir != "" {
v.AddConfigPath(envDir) viper.AddConfigPath(envDir)
} }
v.AddConfigPath(".") viper.AddConfigPath(".")
homeDir, err := os.UserHomeDir() homeDir, err := os.UserHomeDir()
if err == nil { if err == nil {
v.AddConfigPath(homeDir + "/.config/ytdl-web") viper.AddConfigPath(homeDir + "/.config/ytdl-web")
} }
v.AddConfigPath(xdg.ConfigHome + "/ytdl-web") viper.AddConfigPath(xdg.ConfigHome + "/ytdl-web")
for _, dir := range xdg.ConfigDirs { for _, dir := range xdg.ConfigDirs {
v.AddConfigPath(dir + "/ytdl-web") viper.AddConfigPath(dir + "/ytdl-web")
} }
} }
if err := v.ReadInConfig(); err != nil { if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); !ok { if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
return nil, err return nil, err
} }
} }
config := DefaultConfig() config := DefaultConfig()
if err := v.Unmarshal(config); err != nil { if err := viper.Unmarshal(config); err != nil {
return nil, err return nil, err
} }

View File

@ -3,11 +3,11 @@
"devenv": { "devenv": {
"locked": { "locked": {
"dir": "src/modules", "dir": "src/modules",
"lastModified": 1689175844, "lastModified": 1708141957,
"narHash": "sha256-+ZAcAnogqNXz5P2/NiZonmgUiv+vCC7/swiSepyTulc=", "narHash": "sha256-IWkw+jsVpu7HFNPbOTJaQeMYQ5/eh7ZVScPvtlSo8vc=",
"owner": "cachix", "owner": "cachix",
"repo": "devenv", "repo": "devenv",
"rev": "db59403d5bdad71dce137705ed7cb926681e5f95", "rev": "40b567388381137a3c49acdff5f4b6946d645a5f",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -20,11 +20,11 @@
"flake-compat": { "flake-compat": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1673956053, "lastModified": 1696426674,
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"owner": "edolstra", "owner": "edolstra",
"repo": "flake-compat", "repo": "flake-compat",
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -38,11 +38,11 @@
"systems": "systems" "systems": "systems"
}, },
"locked": { "locked": {
"lastModified": 1685518550, "lastModified": 1701680307,
"narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=", "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef", "rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -59,11 +59,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1660459072, "lastModified": 1703887061,
"narHash": "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=", "narHash": "sha256-gGPa9qWNc6eCXT/+Z5/zMkyYOuRZqeFZBDbopNZQkuY=",
"owner": "hercules-ci", "owner": "hercules-ci",
"repo": "gitignore.nix", "repo": "gitignore.nix",
"rev": "a20de23b925fd8264fd7fad6454652e142fd7f73", "rev": "43e1aa1308018f37118e34d3a9cb4f5e75dc11d5",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -74,11 +74,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1689168768, "lastModified": 1708151420,
"narHash": "sha256-mCw3LPg2jJkapvJpkd1IZ8k0IJlSG2ECvz3vcOAu+Uo=", "narHash": "sha256-MGT/4aGCWQPQiu6COqJdCj9kSpLPiShgbwpbC38YXC8=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "6fd9edc94426a3c050ad589c8f033b5ca55454c7", "rev": "6e2f00c83911461438301db0dba5281197fe4b3a",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -90,16 +90,16 @@
}, },
"nixpkgs-stable": { "nixpkgs-stable": {
"locked": { "locked": {
"lastModified": 1685801374, "lastModified": 1704874635,
"narHash": "sha256-otaSUoFEMM+LjBI1XL/xGB5ao6IwnZOXc47qhIgJe8U=", "narHash": "sha256-YWuCrtsty5vVZvu+7BchAxmcYzTMfolSPP5io8+WYCg=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "c37ca420157f4abc31e26f436c1145f8951ff373", "rev": "3dc440faeee9e889fe2d1b4d25ad0f430d449356",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "NixOS",
"ref": "nixos-23.05", "ref": "nixos-23.11",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }
@ -115,11 +115,11 @@
"nixpkgs-stable": "nixpkgs-stable" "nixpkgs-stable": "nixpkgs-stable"
}, },
"locked": { "locked": {
"lastModified": 1688596063, "lastModified": 1708018599,
"narHash": "sha256-9t7RxBiKWHygsqXtiNATTJt4lim/oSYZV3RG8OjDDng=", "narHash": "sha256-M+Ng6+SePmA8g06CmUZWi1AjG2tFBX9WCXElBHEKnyM=",
"owner": "cachix", "owner": "cachix",
"repo": "pre-commit-hooks.nix", "repo": "pre-commit-hooks.nix",
"rev": "c8d18ba345730019c3faf412c96a045ade171895", "rev": "5df5a70ad7575f6601d91f0efec95dd9bc619431",
"type": "github" "type": "github"
}, },
"original": { "original": {

757
flake.lock Normal file
View File

@ -0,0 +1,757 @@
{
"nodes": {
"cachix": {
"inputs": {
"devenv": "devenv_2",
"flake-compat": [
"devenv",
"flake-compat"
],
"git-hooks": [
"devenv",
"pre-commit-hooks"
],
"nixpkgs": [
"devenv",
"nixpkgs"
]
},
"locked": {
"lastModified": 1726520618,
"narHash": "sha256-jOsaBmJ/EtX5t/vbylCdS7pWYcKGmWOKg4QKUzKr6dA=",
"owner": "cachix",
"repo": "cachix",
"rev": "695525f9086542dfb09fde0871dbf4174abbf634",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "cachix",
"type": "github"
}
},
"cachix_2": {
"inputs": {
"devenv": "devenv_3",
"flake-compat": [
"devenv",
"cachix",
"devenv",
"flake-compat"
],
"nixpkgs": [
"devenv",
"cachix",
"devenv",
"nixpkgs"
],
"pre-commit-hooks": [
"devenv",
"cachix",
"devenv",
"pre-commit-hooks"
]
},
"locked": {
"lastModified": 1712055811,
"narHash": "sha256-7FcfMm5A/f02yyzuavJe06zLa9hcMHsagE28ADcmQvk=",
"owner": "cachix",
"repo": "cachix",
"rev": "02e38da89851ec7fec3356a5c04bc8349cae0e30",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "cachix",
"type": "github"
}
},
"devenv": {
"inputs": {
"cachix": "cachix",
"flake-compat": "flake-compat_2",
"nix": "nix_3",
"nixpkgs": "nixpkgs_3",
"pre-commit-hooks": "pre-commit-hooks_2"
},
"locked": {
"lastModified": 1730890834,
"narHash": "sha256-ogmpmsPOlX4qeWVW4NZkTd0Lx8V4rvnjwlgOX7WnTZo=",
"owner": "cachix",
"repo": "devenv",
"rev": "c5353d1a0483b8f0dc15933de91c6b1b9a892831",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "devenv",
"type": "github"
}
},
"devenv_2": {
"inputs": {
"cachix": "cachix_2",
"flake-compat": [
"devenv",
"cachix",
"flake-compat"
],
"nix": "nix_2",
"nixpkgs": [
"devenv",
"cachix",
"nixpkgs"
],
"pre-commit-hooks": [
"devenv",
"cachix",
"git-hooks"
]
},
"locked": {
"lastModified": 1723156315,
"narHash": "sha256-0JrfahRMJ37Rf1i0iOOn+8Z4CLvbcGNwa2ChOAVrp/8=",
"owner": "cachix",
"repo": "devenv",
"rev": "ff5eb4f2accbcda963af67f1a1159e3f6c7f5f91",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "devenv",
"type": "github"
}
},
"devenv_3": {
"inputs": {
"flake-compat": [
"devenv",
"cachix",
"devenv",
"cachix",
"flake-compat"
],
"nix": "nix",
"nixpkgs": "nixpkgs",
"poetry2nix": "poetry2nix",
"pre-commit-hooks": [
"devenv",
"cachix",
"devenv",
"cachix",
"pre-commit-hooks"
]
},
"locked": {
"lastModified": 1708704632,
"narHash": "sha256-w+dOIW60FKMaHI1q5714CSibk99JfYxm0CzTinYWr+Q=",
"owner": "cachix",
"repo": "devenv",
"rev": "2ee4450b0f4b95a1b90f2eb5ffea98b90e48c196",
"type": "github"
},
"original": {
"owner": "cachix",
"ref": "python-rewrite",
"repo": "devenv",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1673956053,
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-compat_2": {
"flake": false,
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": [
"devenv",
"nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1712014858,
"narHash": "sha256-sB4SWl2lX95bExY2gMFG5HIzvva5AVMJd4Igm+GpZNw=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "9126214d0a59633752a136528f5f3b9aa8565b7d",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-parts_2": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1730504689,
"narHash": "sha256-hgmguH29K2fvs9szpq2r3pz2/8cJd2LPS+b4tfNFCwE=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "506278e768c2a08bec68eb62932193e341f55c90",
"type": "github"
},
"original": {
"id": "flake-parts",
"type": "indirect"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1689068808,
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"locked": {
"lastModified": 1667395993,
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_3": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"devenv",
"pre-commit-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"libgit2": {
"flake": false,
"locked": {
"lastModified": 1697646580,
"narHash": "sha256-oX4Z3S9WtJlwvj0uH9HlYcWv+x1hqp8mhXl7HsLu2f0=",
"owner": "libgit2",
"repo": "libgit2",
"rev": "45fd9ed7ae1a9b74b957ef4f337bc3c8b3df01b5",
"type": "github"
},
"original": {
"owner": "libgit2",
"repo": "libgit2",
"type": "github"
}
},
"mk-shell-bin": {
"locked": {
"lastModified": 1677004959,
"narHash": "sha256-/uEkr1UkJrh11vD02aqufCxtbF5YnhRTIKlx5kyvf+I=",
"owner": "rrbutani",
"repo": "nix-mk-shell-bin",
"rev": "ff5d8bd4d68a347be5042e2f16caee391cd75887",
"type": "github"
},
"original": {
"owner": "rrbutani",
"repo": "nix-mk-shell-bin",
"type": "github"
}
},
"nix": {
"inputs": {
"flake-compat": "flake-compat",
"nixpkgs": [
"devenv",
"cachix",
"devenv",
"cachix",
"devenv",
"nixpkgs"
],
"nixpkgs-regression": "nixpkgs-regression"
},
"locked": {
"lastModified": 1712911606,
"narHash": "sha256-BGvBhepCufsjcUkXnEEXhEVjwdJAwPglCC2+bInc794=",
"owner": "domenkozar",
"repo": "nix",
"rev": "b24a9318ea3f3600c1e24b4a00691ee912d4de12",
"type": "github"
},
"original": {
"owner": "domenkozar",
"ref": "devenv-2.21",
"repo": "nix",
"type": "github"
}
},
"nix-github-actions": {
"inputs": {
"nixpkgs": [
"devenv",
"cachix",
"devenv",
"cachix",
"devenv",
"poetry2nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1688870561,
"narHash": "sha256-4UYkifnPEw1nAzqqPOTL2MvWtm3sNGw1UTYTalkTcGY=",
"owner": "nix-community",
"repo": "nix-github-actions",
"rev": "165b1650b753316aa7f1787f3005a8d2da0f5301",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nix-github-actions",
"type": "github"
}
},
"nix2container": {
"inputs": {
"flake-utils": "flake-utils_3",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1730479402,
"narHash": "sha256-79NLeNjpCa4mSasmFsE3QA6obURezF0TUO5Pm+1daog=",
"owner": "nlewo",
"repo": "nix2container",
"rev": "5fb215a1564baa74ce04ad7f903d94ad6617e17a",
"type": "github"
},
"original": {
"owner": "nlewo",
"repo": "nix2container",
"type": "github"
}
},
"nix_2": {
"inputs": {
"flake-compat": [
"devenv",
"cachix",
"devenv",
"flake-compat"
],
"nixpkgs": [
"devenv",
"cachix",
"devenv",
"nixpkgs"
],
"nixpkgs-regression": "nixpkgs-regression_2"
},
"locked": {
"lastModified": 1712911606,
"narHash": "sha256-BGvBhepCufsjcUkXnEEXhEVjwdJAwPglCC2+bInc794=",
"owner": "domenkozar",
"repo": "nix",
"rev": "b24a9318ea3f3600c1e24b4a00691ee912d4de12",
"type": "github"
},
"original": {
"owner": "domenkozar",
"ref": "devenv-2.21",
"repo": "nix",
"type": "github"
}
},
"nix_3": {
"inputs": {
"flake-compat": [
"devenv",
"flake-compat"
],
"flake-parts": "flake-parts",
"libgit2": "libgit2",
"nixpkgs": "nixpkgs_2",
"nixpkgs-23-11": "nixpkgs-23-11",
"nixpkgs-regression": "nixpkgs-regression_3",
"pre-commit-hooks": "pre-commit-hooks"
},
"locked": {
"lastModified": 1727438425,
"narHash": "sha256-X8ES7I1cfNhR9oKp06F6ir4Np70WGZU5sfCOuNBEwMg=",
"owner": "domenkozar",
"repo": "nix",
"rev": "f6c5ae4c1b2e411e6b1e6a8181cc84363d6a7546",
"type": "github"
},
"original": {
"owner": "domenkozar",
"ref": "devenv-2.24",
"repo": "nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1692808169,
"narHash": "sha256-x9Opq06rIiwdwGeK2Ykj69dNc2IvUH1fY55Wm7atwrE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9201b5ff357e781bf014d0330d18555695df7ba8",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-23-11": {
"locked": {
"lastModified": 1717159533,
"narHash": "sha256-oamiKNfr2MS6yH64rUn99mIZjc45nGJlj9eGth/3Xuw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "a62e6edd6d5e1fa0329b8653c801147986f8d446",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "a62e6edd6d5e1fa0329b8653c801147986f8d446",
"type": "github"
}
},
"nixpkgs-lib": {
"locked": {
"lastModified": 1730504152,
"narHash": "sha256-lXvH/vOfb4aGYyvFmZK/HlsNsr/0CVWlwYvo2rxJk3s=",
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/cc2f28000298e1269cea6612cd06ec9979dd5d7f.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/cc2f28000298e1269cea6612cd06ec9979dd5d7f.tar.gz"
}
},
"nixpkgs-regression": {
"locked": {
"lastModified": 1643052045,
"narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
"type": "github"
}
},
"nixpkgs-regression_2": {
"locked": {
"lastModified": 1643052045,
"narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
"type": "github"
}
},
"nixpkgs-regression_3": {
"locked": {
"lastModified": 1643052045,
"narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
"type": "github"
}
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1720386169,
"narHash": "sha256-NGKVY4PjzwAa4upkGtAMz1npHGoRzWotlSnVlqI40mo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "194846768975b7ad2c4988bdb82572c00222c0d7",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-24.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1717432640,
"narHash": "sha256-+f9c4/ZX5MWDOuB1rKoWj+lBNm0z0rs4CK47HBLxy1o=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "88269ab3044128b7c2f4c7d68448b2fb50456870",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "release-24.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1716977621,
"narHash": "sha256-Q1UQzYcMJH4RscmpTkjlgqQDX5yi1tZL0O345Ri6vXQ=",
"owner": "cachix",
"repo": "devenv-nixpkgs",
"rev": "4267e705586473d3e5c8d50299e71503f16a6fb6",
"type": "github"
},
"original": {
"owner": "cachix",
"ref": "rolling",
"repo": "devenv-nixpkgs",
"type": "github"
}
},
"nixpkgs_4": {
"locked": {
"lastModified": 1716977621,
"narHash": "sha256-Q1UQzYcMJH4RscmpTkjlgqQDX5yi1tZL0O345Ri6vXQ=",
"owner": "cachix",
"repo": "devenv-nixpkgs",
"rev": "4267e705586473d3e5c8d50299e71503f16a6fb6",
"type": "github"
},
"original": {
"owner": "cachix",
"ref": "rolling",
"repo": "devenv-nixpkgs",
"type": "github"
}
},
"poetry2nix": {
"inputs": {
"flake-utils": "flake-utils",
"nix-github-actions": "nix-github-actions",
"nixpkgs": [
"devenv",
"cachix",
"devenv",
"cachix",
"devenv",
"nixpkgs"
]
},
"locked": {
"lastModified": 1692876271,
"narHash": "sha256-IXfZEkI0Mal5y1jr6IRWMqK8GW2/f28xJenZIPQqkY0=",
"owner": "nix-community",
"repo": "poetry2nix",
"rev": "d5006be9c2c2417dafb2e2e5034d83fabd207ee3",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "poetry2nix",
"type": "github"
}
},
"pre-commit-hooks": {
"inputs": {
"flake-compat": [
"devenv",
"nix"
],
"flake-utils": "flake-utils_2",
"gitignore": [
"devenv",
"nix"
],
"nixpkgs": [
"devenv",
"nix",
"nixpkgs"
],
"nixpkgs-stable": [
"devenv",
"nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1712897695,
"narHash": "sha256-nMirxrGteNAl9sWiOhoN5tIHyjBbVi5e2tgZUgZlK3Y=",
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "40e6053ecb65fcbf12863338a6dcefb3f55f1bf8",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"type": "github"
}
},
"pre-commit-hooks_2": {
"inputs": {
"flake-compat": [
"devenv",
"flake-compat"
],
"gitignore": "gitignore",
"nixpkgs": [
"devenv",
"nixpkgs"
],
"nixpkgs-stable": "nixpkgs-stable"
},
"locked": {
"lastModified": 1726745158,
"narHash": "sha256-D5AegvGoEjt4rkKedmxlSEmC+nNLMBPWFxvmYnVLhjk=",
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "4e743a6920eab45e8ba0fbe49dc459f1423a4b74",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"type": "github"
}
},
"root": {
"inputs": {
"devenv": "devenv",
"flake-parts": "flake-parts_2",
"mk-shell-bin": "mk-shell-bin",
"nix2container": "nix2container",
"nixpkgs": "nixpkgs_4"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

56
flake.nix Normal file
View File

@ -0,0 +1,56 @@
{
description = "Description for the project";
inputs = {
nixpkgs.url = "github:cachix/devenv-nixpkgs/rolling";
devenv.url = "github:cachix/devenv";
nix2container.url = "github:nlewo/nix2container";
nix2container.inputs.nixpkgs.follows = "nixpkgs";
mk-shell-bin.url = "github:rrbutani/nix-mk-shell-bin";
};
nixConfig = {
extra-trusted-public-keys = "devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw=";
extra-substituters = "https://devenv.cachix.org";
};
outputs = inputs@{ self, flake-parts, ... }:
let
version = "1.2.0";
rev = if (self ? rev) then self.rev else self.dirtyRev;
in
flake-parts.lib.mkFlake { inherit inputs; } {
imports = [
inputs.devenv.flakeModule
];
systems = [ "x86_64-linux" "i686-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ];
perSystem = { config, self', inputs', pkgs, system, ... }: {
# Per-system attributes can be defined here. The self' and inputs'
# module parameters provide easy access to attributes of the same
# system.
# needed for devenv up
packages.devenv-up = self'.devShells.default.config.procfileScript;
packages.default = pkgs.callPackage ./nix/package.nix { inherit rev version; };
devenv.shells.default = {
name = "ytdl-web";
imports = [
./nix/devenv.nix
];
};
};
flake = {
# The usual flake attributes can be defined here, including system-
# agnostic ones like nixosModule and system-enumerating ones, although
# those are more easily expressed in perSystem.
nixosModules.default = import ./nix/module.nix;
};
};
}

3
go.mod
View File

@ -1,13 +1,12 @@
module go.fifitido.net/ytdl-web module go.fifitido.net/ytdl-web
go 1.20 go 1.22
require ( require (
github.com/adrg/xdg v0.4.0 github.com/adrg/xdg v0.4.0
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.0.10
github.com/htfy96/reformism v0.0.0-20160819020323-e5bfca398e73 github.com/htfy96/reformism v0.0.0-20160819020323-e5bfca398e73
github.com/samber/lo v1.38.1
github.com/spf13/cobra v1.7.0 github.com/spf13/cobra v1.7.0
github.com/spf13/viper v1.10.0 github.com/spf13/viper v1.10.0
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53

3
go.sum
View File

@ -36,6 +36,7 @@ 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/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 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/htfy96/reformism v0.0.0-20160819020323-e5bfca398e73 h1:Shcv21tstWAyUkKxbn5bTARYej9sgEgFgTRxUPk1J8o= github.com/htfy96/reformism v0.0.0-20160819020323-e5bfca398e73 h1:Shcv21tstWAyUkKxbn5bTARYej9sgEgFgTRxUPk1J8o=
@ -71,8 +72,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
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=

13
main.go
View File

@ -1,11 +1,16 @@
/* /*
Copyright © 2023 NAME HERE <EMAIL ADDRESS> Copyright © 2024 Evan Fiordeliso <evan.fiordeliso@gmail.com>
*/ */
package main package main
import "go.fifitido.net/ytdl-web/cmd" import (
"os"
"go.fifitido.net/ytdl-web/cmd"
)
func main() { func main() {
cmd.Execute() if err := cmd.Execute(); err != nil {
os.Exit(1)
}
} }

View File

@ -4,7 +4,7 @@
# https://devenv.sh/basics/ # https://devenv.sh/basics/
env.NAME = "ytdl-web"; env.NAME = "ytdl-web";
env.BINARY_OUT = "./out/ytdl-web"; env.BINARY_OUT = "./out/ytdl-web";
env.VERSION = "1.0.8"; env.VERSION = "v1.2.3";
env.VERSION_PKG = "go.fifitido.net/ytdl-web/version"; env.VERSION_PKG = "go.fifitido.net/ytdl-web/version";
env.DOCKER_REGISTRY = "git.fifitido.net"; env.DOCKER_REGISTRY = "git.fifitido.net";
env.DOCKER_ORG = "apps"; env.DOCKER_ORG = "apps";
@ -13,7 +13,7 @@
env.YTDL_ENV = "Development"; env.YTDL_ENV = "Development";
# https://devenv.sh/packages/ # https://devenv.sh/packages/
packages = with pkgs; [ packages = with pkgs; [
git git
air air
goreleaser goreleaser
@ -22,6 +22,7 @@
buildkit buildkit
docker-buildx docker-buildx
yt-dlp yt-dlp
cobra-cli
]; ];
# https://devenv.sh/scripts/ # https://devenv.sh/scripts/
@ -67,7 +68,7 @@
output=''${2:-type=docker} output=''${2:-type=docker}
docker-init docker-init
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 \
@ -76,6 +77,7 @@
--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 \
--output $output --output $output
''; '';
@ -102,4 +104,4 @@
processes.web.exec = "air"; processes.web.exec = "air";
# See full reference at https://devenv.sh/reference/options/ # See full reference at https://devenv.sh/reference/options/
} }

229
nix/module.nix Normal file
View File

@ -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 = { };
};
};
};
}

40
nix/package.nix Normal file
View File

@ -0,0 +1,40 @@
{ 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"
];
nativeBuildInputs = [ installShellFiles ];
postInstall = ''
installShellCompletion --cmd ${pname} \
--zsh <($out/bin/${pname} completion zsh) \
--bash <($out/bin/${pname} completion bash) \
--fish <($out/bin/${pname} completion fish)
'';
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";
};
}

View File

@ -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
}

View File

@ -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
}
}

19
pkg/ytdl/error.go Normal file
View File

@ -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
}

View File

@ -2,6 +2,8 @@ package ytdl
import ( import (
"bytes" "bytes"
"encoding/json"
"fmt"
"io" "io"
"os/exec" "os/exec"
"strings" "strings"
@ -27,7 +29,7 @@ type ytdlImpl struct {
func NewYtdl(cfg *config.Config, logger *slog.Logger, cache cache.MetadataCache) Ytdl { func NewYtdl(cfg *config.Config, logger *slog.Logger, cache cache.MetadataCache) Ytdl {
cmd := exec.Command( cmd := exec.Command(
cfg.BinaryPath, cfg.Ytdlp.BinaryPath,
"--version", "--version",
) )
var out bytes.Buffer var out bytes.Buffer
@ -42,32 +44,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 profile != "" {
sb.WriteByte(':')
sb.WriteString(profile)
}
if container != "" {
sb.WriteByte(':')
sb.WriteByte(':')
sb.WriteString(container)
}
return sb.String()
}
func (y *ytdlImpl) appendCookieArgs(args []string) []string {
if y.cfg.Cookies.Enabled { if y.cfg.Cookies.Enabled {
if y.cfg.Cookies.FromBrowser.Browser != "" { if y.cfg.Cookies.FromBrowser.Browser != "" {
options = append(options, WithBrowserCookies( args = append(args, "--cookies-from-browser", buildBrowserCookieString(
y.cfg.Cookies.FromBrowser.Browser, y.cfg.Cookies.FromBrowser.Browser,
y.cfg.Cookies.FromBrowser.Keyring, y.cfg.Cookies.FromBrowser.Keyring,
y.cfg.Cookies.FromBrowser.Profile, y.cfg.Cookies.FromBrowser.Profile,
y.cfg.Cookies.FromBrowser.Container, y.cfg.Cookies.FromBrowser.Container,
)) ))
} else { } else {
options = append(options, WithCookieFile(y.cfg.Cookies.FilePath)) args = append(args, "--cookies", y.cfg.Cookies.FilePath)
} }
} }
if y.cfg.IsDevelopment() { return args
options = append(options, WithDebug())
}
return options
} }
func (y *ytdlImpl) Version() string { func (y *ytdlImpl) Version() string {
@ -81,14 +95,29 @@ func (y *ytdlImpl) GetMetadata(url string) (*metadata.Metadata, error) {
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()))
cmd := exec.Command(y.cfg.Ytdlp.BinaryPath, args...)
out, err := cmd.Output()
if err != nil {
attrs := []any{
slog.String("url", url),
slog.String("error", err.Error()),
}
y.logger.Error("failed to get metadata", attrs...)
return nil, err
}
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 return nil, err
} }
@ -101,17 +130,37 @@ 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(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.Command(y.cfg.Ytdlp.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 {
y.logger.Error("failed to download", slog.String("url", url), slog.String("error", err.Error()))
return err return err
} }