From 84793d6a6e7010db9d0712077b0d4dd8d2f6d7e1 Mon Sep 17 00:00:00 2001 From: medi0x1 Date: Thu, 4 Dec 2025 23:13:02 +0100 Subject: [PATCH 1/3] Fix Chaturbate rate limit and update ffmpeg handling --- streamonitor/downloaders/ffmpeg.py | 17 ++- streamonitor/sites/chaturbate.py | 203 ++++++++++++++++++++++++++--- 2 files changed, 199 insertions(+), 21 deletions(-) diff --git a/streamonitor/downloaders/ffmpeg.py b/streamonitor/downloaders/ffmpeg.py index 2c8ec6f5..b94b3c67 100644 --- a/streamonitor/downloaders/ffmpeg.py +++ b/streamonitor/downloaders/ffmpeg.py @@ -11,9 +11,24 @@ def getVideoFfmpeg(self, url, filename): cmd = [ FFMPEG_PATH, + '-loglevel', 'warning', # Show warnings and errors '-user_agent', self.headers['User-Agent'] ] + # Build custom headers string + headers_list = [] + if 'Referer' in self.headers: + headers_list.append(f"Referer: {self.headers['Referer']}") + if 'Origin' in self.headers: + headers_list.append(f"Origin: {self.headers['Origin']}") + if 'Accept' in self.headers: + headers_list.append(f"Accept: {self.headers['Accept']}") + + # Add all headers as one option + if headers_list: + headers_str = '\r\n'.join(headers_list) + cmd.extend(['-headers', headers_str]) + if type(self.cookies) is requests.cookies.RequestsCookieJar: cookies_text = '' for cookie in self.cookies: @@ -101,4 +116,4 @@ def execute(): self.stopDownload = lambda: stopping.pls_stop() thread.join() self.stopDownload = None - return not error + return not error \ No newline at end of file diff --git a/streamonitor/sites/chaturbate.py b/streamonitor/sites/chaturbate.py index c5ce25b8..2c2aaa78 100644 --- a/streamonitor/sites/chaturbate.py +++ b/streamonitor/sites/chaturbate.py @@ -1,8 +1,18 @@ import re import requests +import time +import random from streamonitor.bot import Bot from streamonitor.enums import Status +# Disable proxy +import os +os.environ['HTTP_PROXY'] = '' +os.environ['HTTPS_PROXY'] = '' +os.environ['http_proxy'] = '' +os.environ['https_proxy'] = '' +os.environ['NO_PROXY'] = '*' + class Chaturbate(Bot): site = 'Chaturbate' @@ -10,36 +20,189 @@ class Chaturbate(Bot): def __init__(self, username): super().__init__(username) - self.sleep_on_offline = 30 - self.sleep_on_error = 60 - + self.sleep_on_offline = 120 + self.sleep_on_error = 180 + + # Create session and disable proxy + self.session = requests.Session() + self.session.trust_env = False + + # Update headers with Chaturbate-specific requirements + self.headers.update({ + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "Referer": f"https://chaturbate.com/{username}/", + "Origin": "https://chaturbate.com", + "Accept": "*/*", + "Accept-Language": "en-US,en;q=0.9", + "Accept-Encoding": "gzip, deflate, br", + "Connection": "keep-alive", + }) + + self.consecutive_errors = 0 + self.last_request_time = 0 + self.min_request_interval = 20 + self.cookies_initialized = False + def getWebsiteURL(self): return "https://www.chaturbate.com/" + self.username def getVideoUrl(self): - url = self.lastInfo['url'] + """Get the stream URL with proper format""" + url = self.lastInfo.get('url', '') + + if not url: + self.logger.error("No URL in lastInfo!") + return None + + # Clean up the URL - remove escape characters + url = url.replace('\\/', '/') + + self.logger.info(f"Raw stream URL: {url[:100]}") + + # Handle CMAF edge servers if indicated if self.lastInfo.get('cmaf_edge'): url = url.replace('playlist.m3u8', 'playlist_sfm4s.m3u8') url = re.sub('live-.+amlst', 'live-c-fhls/amlst', url) + self.logger.info(f"CMAF adjusted URL: {url[:100]}") + + # Get the best resolution playlist + selected = self.getWantedResolutionPlaylist(url) + if selected: + self.logger.info(f"Final URL: {selected[:100]}") + else: + self.logger.error("Failed to get resolution playlist!") + return selected - return self.getWantedResolutionPlaylist(url) + def _wait_for_rate_limit(self): + """Ensure minimum time between requests""" + current_time = time.time() + time_since_last_request = current_time - self.last_request_time + + if time_since_last_request < self.min_request_interval: + sleep_time = self.min_request_interval - time_since_last_request + jitter = random.uniform(1, 3) + time.sleep(sleep_time + jitter) + + self.last_request_time = time.time() + + def _initialize_cookies(self): + """Visit main page first to get cookies/tokens""" + if self.cookies_initialized: + return True + + try: + headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Language": "en-US,en;q=0.9", + "Accept-Encoding": "gzip, deflate, br", + "Connection": "keep-alive", + "Upgrade-Insecure-Requests": "1", + } + + # Visit the room page first to get cookies + room_url = f"https://chaturbate.com/{self.username}/" + r = self.session.get(room_url, headers=headers, timeout=30) + + if r.status_code == 200: + self.cookies_initialized = True + # Store cookies for ffmpeg to use + self.cookies = r.cookies + return True + else: + return False + + except Exception: + return False def getStatus(self): - headers = {"X-Requested-With": "XMLHttpRequest"} - data = {"room_slug": self.username, "bandwidth": "high"} + """Use AJAX endpoint with proper session handling""" + self._wait_for_rate_limit() + + # Initialize cookies on first run + if not self.cookies_initialized: + if not self._initialize_cookies(): + self.consecutive_errors += 1 + return Status.ERROR + time.sleep(2) + + headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "Accept": "application/json, text/javascript, */*; q=0.01", + "Accept-Language": "en-US,en;q=0.9", + "Accept-Encoding": "gzip, deflate, br", + "X-Requested-With": "XMLHttpRequest", + "Origin": "https://chaturbate.com", + "Referer": f"https://chaturbate.com/{self.username}/", + "Connection": "keep-alive", + } + + data = { + "room_slug": self.username, + "bandwidth": "high" + } try: - r = requests.post("https://chaturbate.com/get_edge_hls_url_ajax/", headers=headers, data=data) - self.lastInfo = r.json() - - if self.lastInfo["room_status"] == "public": - status = Status.PUBLIC - elif self.lastInfo["room_status"] in ["private", "hidden"]: - status = Status.PRIVATE - else: - status = Status.OFFLINE - except: - status = Status.RATELIMIT + r = self.session.post( + "https://chaturbate.com/get_edge_hls_url_ajax/", + headers=headers, + data=data, + timeout=30 + ) + + if r.status_code == 429: + self.consecutive_errors += 1 + self.ratelimit = True + self.sleep_on_error = min(900, 120 * (2 ** self.consecutive_errors)) + return Status.RATELIMIT + + if r.status_code == 403: + self.cookies_initialized = False + self.consecutive_errors += 1 + return Status.RATELIMIT + + if r.status_code != 200: + self.consecutive_errors += 1 + return Status.ERROR + + try: + self.lastInfo = r.json() + room_status = self.lastInfo.get("room_status", "offline") + + if room_status == "public": + url = self.lastInfo.get("url", "") + if url: + # Update cookies from this request too + if r.cookies: + self.cookies.update(r.cookies) + self.consecutive_errors = 0 + return Status.PUBLIC + else: + return Status.OFFLINE + + elif room_status in ["private", "hidden"]: + return Status.PRIVATE + else: + return Status.OFFLINE + + except ValueError: + self.consecutive_errors += 1 + return Status.ERROR + + except requests.exceptions.Timeout: + self.consecutive_errors += 1 + return Status.ERROR + + except requests.exceptions.RequestException: + self.consecutive_errors += 1 + self.cookies_initialized = False + return Status.RATELIMIT + + except Exception: + self.consecutive_errors += 1 + return Status.ERROR - self.ratelimit = status == Status.RATELIMIT - return status + finally: + self.ratelimit = False + if self.consecutive_errors > 0: + self.sleep_on_error = min(900, 120 * (2 ** self.consecutive_errors)) \ No newline at end of file From 7091bbf7b24c5bb231adb20a03720e677723b5af Mon Sep 17 00:00:00 2001 From: medi0x1 Date: Fri, 5 Dec 2025 20:05:00 +0100 Subject: [PATCH 2/3] Update CB handler: cookie normalization & session pacing --- streamonitor/sites/chaturbate.py | 212 ++++++++++++------------------- 1 file changed, 83 insertions(+), 129 deletions(-) diff --git a/streamonitor/sites/chaturbate.py b/streamonitor/sites/chaturbate.py index 2c2aaa78..c4cccc04 100644 --- a/streamonitor/sites/chaturbate.py +++ b/streamonitor/sites/chaturbate.py @@ -4,14 +4,7 @@ import random from streamonitor.bot import Bot from streamonitor.enums import Status - -# Disable proxy -import os -os.environ['HTTP_PROXY'] = '' -os.environ['HTTPS_PROXY'] = '' -os.environ['http_proxy'] = '' -os.environ['https_proxy'] = '' -os.environ['NO_PROXY'] = '*' +from requests.utils import dict_from_cookiejar, cookiejar_from_dict class Chaturbate(Bot): @@ -22,12 +15,9 @@ def __init__(self, username): super().__init__(username) self.sleep_on_offline = 120 self.sleep_on_error = 180 - - # Create session and disable proxy self.session = requests.Session() self.session.trust_env = False - - # Update headers with Chaturbate-specific requirements + self.headers.update({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Referer": f"https://chaturbate.com/{username}/", @@ -37,172 +27,136 @@ def __init__(self, username): "Accept-Encoding": "gzip, deflate, br", "Connection": "keep-alive", }) - + self.consecutive_errors = 0 self.last_request_time = 0 self.min_request_interval = 20 self.cookies_initialized = False - + + def _normalize_cookies(self, jar): + return cookiejar_from_dict(dict_from_cookiejar(jar)) + def getWebsiteURL(self): return "https://www.chaturbate.com/" + self.username - + + def getPlaylistVariants(self, url): + try: + result = self.session.get( + url, + headers=self.headers, + cookies=self.cookies, + timeout=15 + ) + result.raise_for_status() + return super().getPlaylistVariants(m3u_data=result.content.decode("utf-8")) + except Exception: + return [] + def getVideoUrl(self): - """Get the stream URL with proper format""" url = self.lastInfo.get('url', '') - if not url: - self.logger.error("No URL in lastInfo!") return None - - # Clean up the URL - remove escape characters + url = url.replace('\\/', '/') - - self.logger.info(f"Raw stream URL: {url[:100]}") - - # Handle CMAF edge servers if indicated + if self.lastInfo.get('cmaf_edge'): url = url.replace('playlist.m3u8', 'playlist_sfm4s.m3u8') url = re.sub('live-.+amlst', 'live-c-fhls/amlst', url) - self.logger.info(f"CMAF adjusted URL: {url[:100]}") - - # Get the best resolution playlist - selected = self.getWantedResolutionPlaylist(url) - if selected: - self.logger.info(f"Final URL: {selected[:100]}") - else: - self.logger.error("Failed to get resolution playlist!") - return selected + + return self.getWantedResolutionPlaylist(url) def _wait_for_rate_limit(self): - """Ensure minimum time between requests""" - current_time = time.time() - time_since_last_request = current_time - self.last_request_time - - if time_since_last_request < self.min_request_interval: - sleep_time = self.min_request_interval - time_since_last_request - jitter = random.uniform(1, 3) - time.sleep(sleep_time + jitter) - + now = time.time() + since = now - self.last_request_time + + if since < self.min_request_interval: + time.sleep(self.min_request_interval - since + random.uniform(1, 3)) + self.last_request_time = time.time() - + def _initialize_cookies(self): - """Visit main page first to get cookies/tokens""" if self.cookies_initialized: return True - try: - headers = { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", - "Accept-Language": "en-US,en;q=0.9", - "Accept-Encoding": "gzip, deflate, br", - "Connection": "keep-alive", - "Upgrade-Insecure-Requests": "1", - } - - # Visit the room page first to get cookies - room_url = f"https://chaturbate.com/{self.username}/" - r = self.session.get(room_url, headers=headers, timeout=30) - + r = self.session.get( + f"https://chaturbate.com/{self.username}/", + headers={ + "User-Agent": self.headers["User-Agent"], + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Language": "en-US,en;q=0.9", + "Accept-Encoding": "gzip, deflate, br", + "Connection": "keep-alive", + "Upgrade-Insecure-Requests": "1", + }, + timeout=30 + ) if r.status_code == 200: self.cookies_initialized = True - # Store cookies for ffmpeg to use - self.cookies = r.cookies + self.cookies = self._normalize_cookies(r.cookies) return True - else: - return False - + return False except Exception: return False def getStatus(self): - """Use AJAX endpoint with proper session handling""" self._wait_for_rate_limit() - - # Initialize cookies on first run + + self._check_count = getattr(self, "_check_count", 0) + 1 + if self._check_count > 10: + self.cookies_initialized = False + self._check_count = 0 + if not self.cookies_initialized: if not self._initialize_cookies(): self.consecutive_errors += 1 return Status.ERROR time.sleep(2) - - headers = { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", - "Accept": "application/json, text/javascript, */*; q=0.01", - "Accept-Language": "en-US,en;q=0.9", - "Accept-Encoding": "gzip, deflate, br", - "X-Requested-With": "XMLHttpRequest", - "Origin": "https://chaturbate.com", - "Referer": f"https://chaturbate.com/{self.username}/", - "Connection": "keep-alive", - } - - data = { - "room_slug": self.username, - "bandwidth": "high" - } try: r = self.session.post( "https://chaturbate.com/get_edge_hls_url_ajax/", - headers=headers, - data=data, + headers={ + "User-Agent": self.headers["User-Agent"], + "Accept": "application/json, text/javascript, */*; q=0.01", + "Accept-Language": "en-US,en;q=0.9", + "Accept-Encoding": "gzip, deflate, br", + "X-Requested-With": "XMLHttpRequest", + "Origin": "https://chaturbate.com", + "Referer": f"https://chaturbate.com/{self.username}/", + "Connection": "keep-alive", + }, + data={"room_slug": self.username, "bandwidth": "high"}, timeout=30 ) - - if r.status_code == 429: - self.consecutive_errors += 1 - self.ratelimit = True - self.sleep_on_error = min(900, 120 * (2 ** self.consecutive_errors)) - return Status.RATELIMIT - - if r.status_code == 403: + + if r.status_code in (429, 403): self.cookies_initialized = False self.consecutive_errors += 1 return Status.RATELIMIT - + if r.status_code != 200: self.consecutive_errors += 1 return Status.ERROR - - try: - self.lastInfo = r.json() - room_status = self.lastInfo.get("room_status", "offline") - - if room_status == "public": - url = self.lastInfo.get("url", "") - if url: - # Update cookies from this request too - if r.cookies: - self.cookies.update(r.cookies) - self.consecutive_errors = 0 - return Status.PUBLIC - else: - return Status.OFFLINE - - elif room_status in ["private", "hidden"]: - return Status.PRIVATE - else: - return Status.OFFLINE - - except ValueError: - self.consecutive_errors += 1 - return Status.ERROR - - except requests.exceptions.Timeout: - self.consecutive_errors += 1 - return Status.ERROR - - except requests.exceptions.RequestException: - self.consecutive_errors += 1 - self.cookies_initialized = False - return Status.RATELIMIT - + + self.lastInfo = r.json() + status = self.lastInfo.get("room_status", "offline") + + if status == "public" and self.lastInfo.get("url"): + if r.cookies: + self.cookies = self._normalize_cookies(r.cookies) + self.consecutive_errors = 0 + return Status.PUBLIC + + if status in ("private", "hidden"): + return Status.PRIVATE + + return Status.OFFLINE + except Exception: self.consecutive_errors += 1 + self.cookies_initialized = False return Status.ERROR finally: + self.sleep_on_error = min(900, 120 * (2 ** self.consecutive_errors)) self.ratelimit = False - if self.consecutive_errors > 0: - self.sleep_on_error = min(900, 120 * (2 ** self.consecutive_errors)) \ No newline at end of file From ec844c268eda50fbbbe1ff33da2fefdc9bc61f6d Mon Sep 17 00:00:00 2001 From: medi0x1 Date: Wed, 10 Dec 2025 21:47:34 +0100 Subject: [PATCH 3/3] CB: add HLS stall recovery + cookie refresh logic --- streamonitor/sites/chaturbate.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/streamonitor/sites/chaturbate.py b/streamonitor/sites/chaturbate.py index c4cccc04..7c987934 100644 --- a/streamonitor/sites/chaturbate.py +++ b/streamonitor/sites/chaturbate.py @@ -32,6 +32,7 @@ def __init__(self, username): self.last_request_time = 0 self.min_request_interval = 20 self.cookies_initialized = False + self.hls_failures = 0 def _normalize_cookies(self, jar): return cookiejar_from_dict(dict_from_cookiejar(jar)) @@ -53,25 +54,20 @@ def getPlaylistVariants(self, url): return [] def getVideoUrl(self): - url = self.lastInfo.get('url', '') + url = self.lastInfo.get("url", "") if not url: return None - url = url.replace('\\/', '/') - if self.lastInfo.get('cmaf_edge'): url = url.replace('playlist.m3u8', 'playlist_sfm4s.m3u8') url = re.sub('live-.+amlst', 'live-c-fhls/amlst', url) - return self.getWantedResolutionPlaylist(url) def _wait_for_rate_limit(self): now = time.time() since = now - self.last_request_time - if since < self.min_request_interval: time.sleep(self.min_request_interval - since + random.uniform(1, 3)) - self.last_request_time = time.time() def _initialize_cookies(self): @@ -141,9 +137,20 @@ def getStatus(self): self.lastInfo = r.json() status = self.lastInfo.get("room_status", "offline") - if status == "public" and self.lastInfo.get("url"): + if status == "public": + url = self.lastInfo.get("url", "") + if not url: + self.hls_failures += 1 + if self.hls_failures >= 2: + self.cookies_initialized = False + self._initialize_cookies() + self.hls_failures = 0 + return Status.ERROR + if r.cookies: self.cookies = self._normalize_cookies(r.cookies) + + self.hls_failures = 0 self.consecutive_errors = 0 return Status.PUBLIC