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")