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 }