Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -475,17 +475,27 @@ 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
if client == nil {
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
}
Expand Down
11 changes: 11 additions & 0 deletions cmd/youtubedr/downloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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
}

Expand Down
6 changes: 4 additions & 2 deletions cmd/youtubedr/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
}

Expand Down
105 changes: 105 additions & 0 deletions cookies.go
Original file line number Diff line number Diff line change
@@ -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
}