diff --git a/client.go b/client.go index 47205665..16e58706 100644 --- a/client.go +++ b/client.go @@ -475,6 +475,20 @@ func (c *Client) GetStreamURLContext(ctx context.Context, video *Video, format * return uri, err } +func (c *Client) SetCookiesFromFile(path string) error { + if c.HTTPClient == nil { + c.HTTPClient = http.DefaultClient + } + + jar, err := readCookies(path) + if err != nil { + return err + } + c.HTTPClient.Jar = jar + + return nil +} + // httpDo sends an HTTP request and returns an HTTP response. func (c *Client) httpDo(req *http.Request) (*http.Response, error) { client := c.HTTPClient @@ -482,10 +496,6 @@ func (c *Client) httpDo(req *http.Request) (*http.Response, error) { client = http.DefaultClient } - req.Header.Set("User-Agent", c.client.userAgent) - req.Header.Set("Origin", "https://youtube.com") - req.Header.Set("Sec-Fetch-Mode", "navigate") - if len(c.consentID) == 0 { c.consentID = strconv.Itoa(rand.Intn(899) + 100) //nolint:gosec } diff --git a/cmd/youtubedr/downloader.go b/cmd/youtubedr/downloader.go index fa4c37ff..5f538269 100644 --- a/cmd/youtubedr/downloader.go +++ b/cmd/youtubedr/downloader.go @@ -6,9 +6,12 @@ import ( "net" "net/http" "net/url" + "os" "strconv" "time" + "log/slog" + "github.com/spf13/pflag" "golang.org/x/net/http/httpproxy" @@ -65,6 +68,14 @@ func getDownloader() *ytdl.Downloader { } downloader.HTTPClient = &http.Client{Transport: httpTransport} + // load cookies + if cookiesPath != "" { + if err := downloader.SetCookiesFromFile(cookiesPath); err != nil { + slog.Error("unable to read cookie file", "error", err) + os.Exit(1) + } + } + return downloader } diff --git a/cmd/youtubedr/root.go b/cmd/youtubedr/root.go index 521d1d1f..40a6d230 100644 --- a/cmd/youtubedr/root.go +++ b/cmd/youtubedr/root.go @@ -10,8 +10,9 @@ import ( ) var ( - cfgFile string - logLevel string + cfgFile string + logLevel string + cookiesPath string ) // rootCmd represents the base command when called without any subcommands @@ -36,6 +37,7 @@ func init() { rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.youtubedr.yaml)") rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "info", "Set log level (error/warn/info/debug)") + rootCmd.PersistentFlags().StringVar(&cookiesPath, "cookies", "", "path to cookies file, see https://everything.curl.dev/http/cookies/fileformat") rootCmd.PersistentFlags().BoolVar(&insecureSkipVerify, "insecure", false, "Skip TLS server certificate verification") } diff --git a/cookies.go b/cookies.go new file mode 100644 index 00000000..952a1bdb --- /dev/null +++ b/cookies.go @@ -0,0 +1,105 @@ +package youtube + +import ( + "bufio" + "fmt" + "log" + "log/slog" + "net/http" + "net/url" + "os" + "strconv" + "strings" + "time" +) + +type Cookies map[string]*http.Cookie + +const cookieDomain = ".youtube.com" + +// SetCookies handles the receipt of the cookies in a reply for the +// given URL. It may or may not choose to save the cookies, depending +// on the jar's policy and implementation. +func (c Cookies) SetCookies(u *url.URL, cookies []*http.Cookie) { + if !c.matchesDomain(u) { + return + } + + for _, cookie := range cookies { + c.setCookie(cookie) + } +} + +func (c Cookies) setCookie(cookie *http.Cookie) { + if cookie.Domain != cookieDomain || cookie.Path != "/" { + return + } + + if cookie.Expires.IsZero() { + slog.Info("delete cookie", "name", cookie.Name) + delete(c, cookie.Name) + } else { + slog.Info("set cookie", "name", cookie.Name) + c[cookie.Name] = cookie + } +} + +func (c Cookies) matchesDomain(u *url.URL) bool { + return strings.HasSuffix(u.Host, cookieDomain) +} + +// Cookies returns the cookies to send in a request for the given URL. +// It is up to the implementation to honor the standard cookie use +// restrictions such as in RFC 6265. +func (c Cookies) Cookies(u *url.URL) (result []*http.Cookie) { + if !c.matchesDomain(u) { + return nil + } + + log.Println("asking for", u) + for _, cookie := range c { + result = append(result, cookie) + } + + return +} + +func readCookies(path string) (http.CookieJar, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + + jar := Cookies{} + scanner := bufio.NewScanner(file) + + // optionally, resize scanner's capacity for lines over 64K, see next example + for scanner.Scan() { + line := scanner.Text() + if !strings.HasPrefix(line, ".") { + continue + } + + fields := strings.Split(line, "\t") + ts, _ := strconv.ParseInt(fields[4], 10, 64) + + if length := len(fields); length < 7 { + return nil, fmt.Errorf("not enough fields in cookie file expected >= 7, is = %d", length) + } + + jar.setCookie(&http.Cookie{ + Domain: fields[0], + Path: fields[2], + Expires: time.Unix(int64(ts), 0), + Name: fields[5], + Value: fields[6], + }) + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return jar, nil +}