diff --git a/README.md b/README.md index 76f731f..9cc18d6 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,8 @@ GLOBAL OPTIONS: --split-filesize value, --sf value size in MB to split each video into segments ('0' to disable) (default: 0) --log-level value log level, availables: 'DEBUG', 'INFO', 'WARN', 'ERROR' (default: "INFO") --port value port to expose the web interface and API (default: "8080") + --cf-cookie value Cloudflare cookie to bypass anti-bot page + --user-agent value Custom user agent for when using cf-cookie --help, -h show help --version, -v print the version ``` @@ -209,6 +211,12 @@ A: Your network is unstable or being blocked by Chaturbate, the program can't he   +**Q: `I'm receiving a message about CloudFlare anti-bot, what do I need to do?`** + +A: You need to successfully pass the CloudFlare anti-bot check and retrieve the cf_clearance Cookie that is set in the browser after successfully passing the check. This MUST be done from the same IP address and the same User-Agent string MUST be provided to chaturbate-dvr. Provide the cookie value and User-Agent string with the --cf-cookie and --user-agent command line options. The Cookie does expire, but it looks like it's Age is at ~1 year. + +  + ## 💬 Verbose Log Change `-log-level` to `DEBUG` to see more details in terminal, like Duration and Size. diff --git a/chaturbate/channel.go b/chaturbate/channel.go index 20f2c8a..7ff6784 100644 --- a/chaturbate/channel.go +++ b/chaturbate/channel.go @@ -27,6 +27,8 @@ type Channel struct { filenamePattern string LastStreamedAt string Interval int + CFCookie string + UserAgent string Framerate int Resolution int ResolutionFallback string @@ -60,6 +62,7 @@ type Channel struct { // Run func (w *Channel) Run() { + if w.Username == "" { w.log(LogTypeError, "username is empty, use `-u USERNAME` to specify") return @@ -98,8 +101,11 @@ func (w *Channel) Run() { w.log(LogTypeError, "release file: %v", err) } } - - w.log(LogTypeInfo, "channel is offline, check again %d min(s) later", w.Interval) + if strings.Contains(body, "Just a moment...") { + w.log(logTypeError, "Cloudflare anti-bot page detected, Try providing cf-cookie and user-agent (Check GitHub for instructions)... Exiting") + os.Exit(1) + } + w.log(logTypeInfo, "channel is offline, check again %d min(s) later", w.Interval) <-time.After(time.Duration(w.Interval) * time.Minute) // minutes cooldown to check online status } } diff --git a/chaturbate/channel_internal.go b/chaturbate/channel_internal.go index 0d2c09f..fb54a94 100644 --- a/chaturbate/channel_internal.go +++ b/chaturbate/channel_internal.go @@ -18,14 +18,30 @@ import ( // requestChannelBody requests the channel page and returns the body. func (w *Channel) requestChannelBody() (string, error) { + transport := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } client := &http.Client{Transport: transport} - resp, err := client.Get(w.ChannelURL) + req, err := http.NewRequest("GET", w.ChannelURL, nil) if err != nil { - return "", fmt.Errorf("client get: %w", err) + return "", fmt.Errorf("new request: %w", err) + } + if w.CFCookie != "" { + cookie := &http.Cookie{ + Name: "cf_clearance", + Value: w.CFCookie, + } + + req.AddCookie(cookie) + } + if w.UserAgent != "" { + req.Header.Set("User-Agent", w.UserAgent) + } + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("client do: %w", err) } defer resp.Body.Close() @@ -33,7 +49,7 @@ func (w *Channel) requestChannelBody() (string, error) { if err != nil { return "", fmt.Errorf("read body: %w", err) } - + return string(body), nil } @@ -100,9 +116,25 @@ func (w *Channel) resolveSource(body string) (string, string, error) { } client := &http.Client{Transport: transport} - resp, err := client.Get(roomData.HLSSource) + req, err := http.NewRequest("GET", roomData.HLSSource, nil) if err != nil { - return "", "", fmt.Errorf("client get: %w", err) + return "", "", fmt.Errorf("new request: %w", err) + } + + if w.CFCookie != "" { + cookie := &http.Cookie{ + Name: "cf_clearance", + Value: w.CFCookie, + } + + req.AddCookie(cookie) + } + if w.UserAgent != "" { + req.Header.Set("User-Agent", w.UserAgent) + } + resp, err := client.Do(req) + if err != nil { + return "", "", fmt.Errorf("client do: %w", err) } if resp.StatusCode != http.StatusOK { switch resp.StatusCode { @@ -196,7 +228,8 @@ func (w *Channel) resolveSource(body string) (string, string, error) { return rootURL, sourceURL, nil } -// mergeSegments runs in the background and merges segments from the buffer to the file. +// mergeSegments is a async function that runs in background for the channel, +// and it merges the segments from buffer to the file. func (w *Channel) mergeSegments() { var segmentRetries int startTime := time.Now() // Track the start time of the current segment. @@ -339,9 +372,25 @@ func (w *Channel) requestChunks() ([]*m3u8.MediaSegment, float64, error) { return nil, 0, fmt.Errorf("channel seems to be paused?") } - resp, err := client.Get(w.sourceURL) + req, err := http.NewRequest("GET", w.sourceURL, nil) if err != nil { - return nil, 3, fmt.Errorf("client get: %w", err) + return nil, 0, fmt.Errorf("new request: %w", err) + } + + if w.CFCookie != "" { + cookie := &http.Cookie{ + Name: "cf_clearance", + Value: w.CFCookie, + } + + req.AddCookie(cookie) + } + if w.UserAgent != "" { + req.Header.Set("User-Agent", w.UserAgent) + } + resp, err := client.Do(req) + if err != nil { + return nil, 3, fmt.Errorf("client do: %w", err) } if resp.StatusCode != http.StatusOK { switch resp.StatusCode { @@ -372,6 +421,7 @@ func (w *Channel) requestChunks() ([]*m3u8.MediaSegment, float64, error) { return chunks, playlist.TargetDuration, nil } + // requestSegment requests the specific single segment and put it into the buffer. // the mergeSegments function will merge the segment from buffer to the file in the backgrond. func (w *Channel) requestSegment(url string, index int) error { @@ -384,9 +434,25 @@ func (w *Channel) requestSegment(url string, index int) error { return fmt.Errorf("channel seems to be paused?") } - resp, err := client.Get(w.rootURL + url) + req, err := http.NewRequest("GET", w.rootURL+url, nil) if err != nil { - return fmt.Errorf("client get: %w", err) + return fmt.Errorf("new request: %w", err) + } + + if w.CFCookie != "" { + cookie := &http.Cookie{ + Name: "cf_clearance", + Value: w.CFCookie, + } + + req.AddCookie(cookie) + } + if w.UserAgent != "" { + req.Header.Set("User-Agent", w.UserAgent) + } + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("client do: %w", err) } if resp.StatusCode != http.StatusOK { return fmt.Errorf("received status code %d", resp.StatusCode) @@ -407,3 +473,4 @@ func (w *Channel) requestSegment(url string, index int) error { return nil } + diff --git a/chaturbate/manager.go b/chaturbate/manager.go index f9dc85d..daf9959 100644 --- a/chaturbate/manager.go +++ b/chaturbate/manager.go @@ -4,7 +4,6 @@ import ( "encoding/json" "errors" "os" - "github.com/google/uuid" "github.com/urfave/cli/v2" ) @@ -32,6 +31,8 @@ type Config struct { SplitDuration int SplitFilesize int Interval int + CFCookie string + UserAgent string } // Manager @@ -39,10 +40,12 @@ type Manager struct { cli *cli.Context Channels map[string]*Channel Updates map[string]chan *Update + } // NewManager func NewManager(c *cli.Context) *Manager { + return &Manager{ cli: c, Channels: map[string]*Channel{}, @@ -101,6 +104,8 @@ func (m *Manager) CreateChannel(conf *Config) error { Resolution: conf.Resolution, ResolutionFallback: conf.ResolutionFallback, Interval: conf.Interval, + CFCookie: m.cli.String("cf-cookie"), + UserAgent: m.cli.String("user-agent"), LastStreamedAt: "-", SegmentDuration: 0, SplitDuration: conf.SplitDuration, diff --git a/go.mod b/go.mod index ab9095a..10d59e0 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/teacat/chaturbate-dvr -go 1.22.0 +go 1.22 require ( github.com/gin-gonic/gin v1.9.1 diff --git a/main.go b/main.go index 9abbb81..9e77385 100644 --- a/main.go +++ b/main.go @@ -104,6 +104,16 @@ func main() { Usage: "minutes to check if the channel is online", Value: 1, }, + &cli.StringFlag{ + Name: "cf-cookie", + Usage: "Cloudflare cookie to bypass anti-bot page", + Value: "", + }, + &cli.StringFlag{ + Name: "user-agent", + Usage: "Custom user agent for when using cf-cookie", + Value: "", + }, //&cli.StringFlag{ // Name: "gui", // Usage: "enabling GUI, availables: 'no', 'web'", @@ -119,6 +129,9 @@ func main() { func start(c *cli.Context) error { fmt.Println(logo) + if c.String("cf-cookie") != "" && c.String("user-agent") == ""{ + return fmt.Errorf("When using the cf-cookie option a user-agent MUST be supplied") + } //if c.String("gui") == "web" { if c.String("username") == "" { @@ -135,6 +148,8 @@ func start(c *cli.Context) error { SplitDuration: c.Int("split-duration"), SplitFilesize: c.Int("split-filesize"), Interval: c.Int("interval"), + CFCookie: c.String("cf-cookie"), + UserAgent: c.String("user-agent"), }); err != nil { return err } @@ -159,7 +174,6 @@ func startWeb(c *cli.Context) error { if err != nil { log.Fatalln(err) } - guiUsername := c.String("gui-username") guiPassword := c.String("gui-password")