Fix config loading and fix/simplify yt-dlp command handling
This commit is contained in:
parent
47caf17973
commit
681225c2ae
|
@ -5,7 +5,6 @@ import (
|
|||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/spf13/viper"
|
||||
|
@ -109,6 +108,10 @@ func (c *DownloadController) ListDownloadLinks(w http.ResponseWriter, r *http.Re
|
|||
}, layout...)
|
||||
}
|
||||
|
||||
var (
|
||||
BUF_LEN = 1024
|
||||
)
|
||||
|
||||
func (c *DownloadController) ProxyDownload(w http.ResponseWriter, r *http.Request) {
|
||||
videoUrl, ok := c.getUrlParam(r)
|
||||
if !ok {
|
||||
|
@ -159,33 +162,19 @@ func (c *DownloadController) ProxyDownload(w http.ResponseWriter, r *http.Reques
|
|||
index = -1
|
||||
}
|
||||
|
||||
pread, pwrite := io.Pipe()
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
read, write := io.Pipe()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
if err := c.ytdl.Download(pwrite, 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)
|
||||
pwrite.CloseWithError(err)
|
||||
return
|
||||
} else {
|
||||
pwrite.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
_, err = io.Copy(w, pread)
|
||||
_, err := io.Copy(w, read)
|
||||
if err != nil {
|
||||
slog.Error("Failed to copy", slog.String("error", err.Error()))
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
if err := c.ytdl.Download(write, videoUrl, format.FormatID, index); err != nil {
|
||||
slog.Error("Failed to download", slog.String("error", err.Error()))
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
write.Close()
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package config
|
|||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -10,11 +11,11 @@ import (
|
|||
)
|
||||
|
||||
type Config struct {
|
||||
Env string `mapstructure:"env"`
|
||||
BinaryPath string `mapstructure:"binaryPath"`
|
||||
HTTP ConfigHTTP `mapstructure:"http"`
|
||||
Cache ConfigCache `mapstructure:"cache"`
|
||||
Cookies ConfigCookies `mapstructure:"cookies"`
|
||||
Env string `mapstructure:"env"`
|
||||
Ytdlp ConfigYtdlp `mapstructure:"ytdlp"`
|
||||
HTTP ConfigHTTP `mapstructure:"http"`
|
||||
Cache ConfigCache `mapstructure:"cache"`
|
||||
Cookies ConfigCookies `mapstructure:"cookies"`
|
||||
}
|
||||
|
||||
func (c *Config) IsProduction() bool {
|
||||
|
@ -29,6 +30,10 @@ func (c *Config) IsStaging() bool {
|
|||
return c.Env == "Staging"
|
||||
}
|
||||
|
||||
type ConfigYtdlp struct {
|
||||
BinaryPath string `mapstructure:"binaryPath"`
|
||||
}
|
||||
|
||||
type ConfigHTTP struct {
|
||||
Port int `mapstructure:"port"`
|
||||
Listen string `mapstructure:"listen"`
|
||||
|
@ -56,8 +61,8 @@ type ConfigCookiesFromBrowser struct {
|
|||
|
||||
func DefaultConfig() *Config {
|
||||
return &Config{
|
||||
Env: "Production",
|
||||
BinaryPath: "yt-dlp",
|
||||
Env: "Production",
|
||||
Ytdlp: ConfigYtdlp{BinaryPath: "yt-dlp"},
|
||||
HTTP: ConfigHTTP{
|
||||
Port: 8080,
|
||||
Listen: "127.0.0.1",
|
||||
|
@ -76,46 +81,46 @@ func DefaultConfig() *Config {
|
|||
}
|
||||
|
||||
func LoadConfig(paths ...string) (*Config, error) {
|
||||
v := viper.New()
|
||||
viper.SetEnvPrefix("YTDL")
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
viper.AutomaticEnv()
|
||||
|
||||
v.SetEnvPrefix("YTDL")
|
||||
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
v.AutomaticEnv()
|
||||
|
||||
v.SetConfigName("config")
|
||||
v.SetConfigType("yaml")
|
||||
viper.SetConfigName("config")
|
||||
viper.SetConfigType("yaml")
|
||||
|
||||
if len(paths) > 0 {
|
||||
for _, path := range paths {
|
||||
v.AddConfigPath(path)
|
||||
for _, p := range paths {
|
||||
viper.AddConfigPath(path.Dir(p))
|
||||
}
|
||||
} else {
|
||||
envDir := os.Getenv("YTDL_CONFIGDIR")
|
||||
if envDir != "" {
|
||||
v.AddConfigPath(envDir)
|
||||
viper.AddConfigPath(envDir)
|
||||
}
|
||||
|
||||
v.AddConfigPath(".")
|
||||
viper.AddConfigPath(".")
|
||||
|
||||
homeDir, err := os.UserHomeDir()
|
||||
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 {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
viper.Debug()
|
||||
|
||||
config := DefaultConfig()
|
||||
if err := v.Unmarshal(config); err != nil {
|
||||
if err := viper.Unmarshal(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
|
@ -1,56 +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
|
||||
}
|
||||
|
||||
var stderr = new(bytes.Buffer)
|
||||
if opts.stderr != nil {
|
||||
cmd.Stderr = stderr
|
||||
}
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return &Error{
|
||||
stderr: stderr.String(),
|
||||
child: 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,99 +0,0 @@
|
|||
package ytdl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"go.fifitido.net/ytdl-web/pkg/ytdl/metadata"
|
||||
)
|
||||
|
||||
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 WithPlaylistIndex(index int) Option {
|
||||
return func(opts *Options) error {
|
||||
opts.args = append(opts.args, "--playlist-items", fmt.Sprint(index+1))
|
||||
return nil
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
package ytdl
|
||||
|
||||
type Error struct {
|
||||
stderr string
|
||||
child error
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
return e.child.Error()
|
||||
}
|
||||
|
||||
func (e *Error) Stderr() string {
|
||||
return e.stderr
|
||||
}
|
102
pkg/ytdl/ytdl.go
102
pkg/ytdl/ytdl.go
|
@ -2,7 +2,8 @@ package ytdl
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
@ -28,7 +29,7 @@ type ytdlImpl struct {
|
|||
|
||||
func NewYtdl(cfg *config.Config, logger *slog.Logger, cache cache.MetadataCache) Ytdl {
|
||||
cmd := exec.Command(
|
||||
cfg.BinaryPath,
|
||||
cfg.Ytdlp.BinaryPath,
|
||||
"--version",
|
||||
)
|
||||
var out bytes.Buffer
|
||||
|
@ -43,28 +44,44 @@ func NewYtdl(cfg *config.Config, logger *slog.Logger, cache cache.MetadataCache)
|
|||
}
|
||||
}
|
||||
|
||||
func (y *ytdlImpl) baseOptions(url string) []Option {
|
||||
options := []Option{}
|
||||
func buildBrowserCookieString(browser, keyring, profile, container string) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(browser)
|
||||
|
||||
metadata, err := y.cache.Get(url)
|
||||
if err == nil {
|
||||
options = append(options, WithLoadJson(metadata))
|
||||
if keyring != "" {
|
||||
sb.WriteByte('+')
|
||||
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.FromBrowser.Browser != "" {
|
||||
options = append(options, WithBrowserCookies(
|
||||
args = append(args, "--cookies-from-browser", buildBrowserCookieString(
|
||||
y.cfg.Cookies.FromBrowser.Browser,
|
||||
y.cfg.Cookies.FromBrowser.Keyring,
|
||||
y.cfg.Cookies.FromBrowser.Profile,
|
||||
y.cfg.Cookies.FromBrowser.Container,
|
||||
))
|
||||
} else {
|
||||
options = append(options, WithCookieFile(y.cfg.Cookies.FilePath))
|
||||
args = append(args, "--cookies", y.cfg.Cookies.FilePath)
|
||||
}
|
||||
}
|
||||
|
||||
return options
|
||||
return args
|
||||
}
|
||||
|
||||
func (y *ytdlImpl) Version() string {
|
||||
|
@ -78,27 +95,32 @@ func (y *ytdlImpl) GetMetadata(url string) (*metadata.Metadata, error) {
|
|||
return meta, nil
|
||||
}
|
||||
|
||||
meta = &metadata.Metadata{}
|
||||
options := append(
|
||||
y.baseOptions(url),
|
||||
WithDumpJson(meta),
|
||||
)
|
||||
args := []string{
|
||||
url,
|
||||
"--dump-single-json",
|
||||
}
|
||||
|
||||
if err := Exec(y.cfg.BinaryPath, url, options...); err != nil {
|
||||
args = y.appendCookieArgs(args)
|
||||
|
||||
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()),
|
||||
}
|
||||
|
||||
var ytdlErr *Error
|
||||
if ok := errors.As(err, &ytdlErr); ok {
|
||||
attrs = append(attrs, slog.String("stderr", ytdlErr.Stderr()))
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if err := y.cache.Set(url, meta, y.cfg.Cache.TTL); err != nil {
|
||||
y.logger.Warn("failed to cache metadata", slog.String("url", url), slog.String("error", err.Error()))
|
||||
}
|
||||
|
@ -108,17 +130,37 @@ func (y *ytdlImpl) GetMetadata(url string) (*metadata.Metadata, error) {
|
|||
|
||||
// Download implements Ytdl
|
||||
func (y *ytdlImpl) Download(w io.Writer, url, format string, index int) error {
|
||||
options := append(
|
||||
y.baseOptions(url),
|
||||
WithFormat(format),
|
||||
WithStreamOutput(w),
|
||||
)
|
||||
|
||||
if index >= 0 {
|
||||
options = append(options, WithPlaylistIndex(index))
|
||||
args := []string{
|
||||
url,
|
||||
"--format", format,
|
||||
"--output", "-",
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue