package ytdl import ( "bytes" "encoding/json" "errors" "fmt" "io" "os/exec" "strings" "log/slog" "go.fifitido.net/ytdl-web/pkg/ytdl/cache" "go.fifitido.net/ytdl-web/pkg/ytdl/metadata" ) 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 logger *slog.Logger cache cache.MetadataCache version string } var defaultYtdl Ytdl = &ytdlImpl{} func SetDefault(y Ytdl) { defaultYtdl = y } func Default() Ytdl { return defaultYtdl } func NewYtdl(cfg *Config, logger *slog.Logger, cache cache.MetadataCache) Ytdl { cmd := exec.Command( cfg.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.CookiesEnabled { if y.cfg.CookiesBrowser != "" { args = append(args, "--cookies-from-browser", buildBrowserCookieString( y.cfg.CookiesBrowser, y.cfg.CookiesBrowserKeyring, y.cfg.CookiesBrowserProfile, y.cfg.CookiesBrowserContainer, )) } else { args = append(args, "--cookies", y.cfg.CookiesFilePath) } } 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) fmt.Printf("ytdlp args: %#v\n", args) cmd := exec.Command(y.cfg.BinaryPath, args...) out, err := cmd.Output() if err != nil { attrs := []any{ slog.String("url", url), } exiterr := &exec.ExitError{} if errors.As(err, &exiterr) { attrs = append(attrs, slog.Int("code", exiterr.ExitCode())) attrs = append(attrs, slog.String("stderr", string(exiterr.Stderr))) attrs = append(attrs, slog.String("error", exiterr.Error())) } else { attrs = append(attrs, slog.String("error", err.Error())) } y.logger.Error("failed to get metadata", attrs...) return nil, err } meta = &metadata.Metadata{} if err := json.Unmarshal(out, meta); err != nil { y.logger.Error("failed to unmarshal metadata", slog.String("url", url), slog.String("error", err.Error())) return nil, err } if err := y.cache.Set(url, meta); err != nil { y.logger.Warn("failed to cache metadata", slog.String("url", url), slog.String("error", err.Error())) } 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.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 }