169 lines
3.4 KiB
Go
169 lines
3.4 KiB
Go
package ytdl
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os/exec"
|
|
"strings"
|
|
|
|
"go.fifitido.net/ytdl-web/config"
|
|
"go.fifitido.net/ytdl-web/pkg/ytdl/cache"
|
|
"go.fifitido.net/ytdl-web/pkg/ytdl/metadata"
|
|
"golang.org/x/exp/slog"
|
|
)
|
|
|
|
type Ytdl interface {
|
|
GetMetadata(url string) (*metadata.Metadata, error)
|
|
Download(w io.Writer, url, format string, index int) error
|
|
Version() string
|
|
}
|
|
|
|
type ytdlImpl struct {
|
|
cfg *config.Config
|
|
logger *slog.Logger
|
|
cache cache.MetadataCache
|
|
version string
|
|
}
|
|
|
|
func NewYtdl(cfg *config.Config, logger *slog.Logger, cache cache.MetadataCache) Ytdl {
|
|
cmd := exec.Command(
|
|
cfg.Ytdlp.BinaryPath,
|
|
"--version",
|
|
)
|
|
var out bytes.Buffer
|
|
cmd.Stdout = &out
|
|
_ = cmd.Run()
|
|
|
|
return &ytdlImpl{
|
|
cfg: cfg,
|
|
logger: logger.With(slog.String("module", "ytdl")),
|
|
cache: cache,
|
|
version: strings.TrimSpace(out.String()),
|
|
}
|
|
}
|
|
|
|
func buildBrowserCookieString(browser, keyring, profile, container string) string {
|
|
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)
|
|
}
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
func (y *ytdlImpl) appendCookieArgs(args []string) []string {
|
|
if y.cfg.Cookies.Enabled {
|
|
if y.cfg.Cookies.FromBrowser.Browser != "" {
|
|
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 {
|
|
args = append(args, "--cookies", y.cfg.Cookies.FilePath)
|
|
}
|
|
}
|
|
|
|
return args
|
|
}
|
|
|
|
func (y *ytdlImpl) Version() string {
|
|
return y.version
|
|
}
|
|
|
|
// GetMetadata implements Ytdl
|
|
func (y *ytdlImpl) GetMetadata(url string) (*metadata.Metadata, error) {
|
|
meta, err := y.cache.Get(url)
|
|
if err == nil {
|
|
return meta, nil
|
|
}
|
|
|
|
args := []string{
|
|
url,
|
|
"--dump-single-json",
|
|
}
|
|
|
|
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()),
|
|
}
|
|
|
|
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()))
|
|
}
|
|
|
|
return meta, nil
|
|
}
|
|
|
|
// Download implements Ytdl
|
|
func (y *ytdlImpl) Download(w io.Writer, url, format string, index int) error {
|
|
args := []string{
|
|
url,
|
|
"--format", format,
|
|
"--output", "-",
|
|
}
|
|
|
|
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 nil
|
|
}
|