try
This commit is contained in:
+24
-22
@@ -1151,21 +1151,11 @@ def select_best_author(content, interest_score):
|
|||||||
logging.error(f"Error in select_best_author: {e}")
|
logging.error(f"Error in select_best_author: {e}")
|
||||||
return random.choice([author["username"] for author in AUTHORS])
|
return random.choice([author["username"] for author in AUTHORS])
|
||||||
|
|
||||||
def check_rate_limit(response):
|
|
||||||
"""Extract rate limit information from Twitter API response headers."""
|
|
||||||
try:
|
|
||||||
remaining = int(response.get('x-rate-limit-remaining', 0))
|
|
||||||
reset = int(response.get('x-rate-limit-reset', 0))
|
|
||||||
return remaining, reset
|
|
||||||
except (ValueError, TypeError) as e:
|
|
||||||
logging.warning(f"Failed to parse rate limit headers: {e}")
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
def check_author_rate_limit(author, max_tweets=17, tweet_window_seconds=86400):
|
def check_author_rate_limit(author, max_tweets=17, tweet_window_seconds=86400):
|
||||||
"""
|
"""
|
||||||
Check if an author is rate-limited for tweets using real-time X API v2 data.
|
Check if an author is rate-limited for tweets using real-time X API v2 data.
|
||||||
Returns (can_post, remaining, reset_timestamp) where can_post is True if tweets are available.
|
Returns (can_post, remaining, reset_timestamp) where can_post is True if tweets are available.
|
||||||
Caches API results in memory for 1 minute.
|
Caches API results in memory for 5 minutes.
|
||||||
Falls back to rate_limit_info.json or assumes 1 tweet remaining if API fails.
|
Falls back to rate_limit_info.json or assumes 1 tweet remaining if API fails.
|
||||||
"""
|
"""
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -1177,7 +1167,7 @@ def check_author_rate_limit(author, max_tweets=17, tweet_window_seconds=86400):
|
|||||||
check_author_rate_limit.cache = {}
|
check_author_rate_limit.cache = {}
|
||||||
|
|
||||||
username = author['username']
|
username = author['username']
|
||||||
cache_key = f"{username}_{int(current_time // 60)}" # Cache for 1 minute
|
cache_key = f"{username}_{int(current_time // 300)}" # Cache for 5 minutes
|
||||||
|
|
||||||
if cache_key in check_author_rate_limit.cache:
|
if cache_key in check_author_rate_limit.cache:
|
||||||
remaining, reset = check_author_rate_limit.cache[cache_key]
|
remaining, reset = check_author_rate_limit.cache[cache_key]
|
||||||
@@ -1215,19 +1205,31 @@ def check_author_rate_limit(author, max_tweets=17, tweet_window_seconds=86400):
|
|||||||
def get_next_author_round_robin():
|
def get_next_author_round_robin():
|
||||||
"""
|
"""
|
||||||
Select the next author using round-robin, respecting real-time X API rate limits.
|
Select the next author using round-robin, respecting real-time X API rate limits.
|
||||||
Returns None if no author is available.
|
Persists the last selected author index to ensure fair rotation across runs.
|
||||||
|
Returns an author dict or None if no authors are available.
|
||||||
"""
|
"""
|
||||||
from foodie_config import AUTHORS
|
|
||||||
global round_robin_index
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
state_file = '/home/shane/foodie_automator/author_state.json'
|
||||||
|
|
||||||
for _ in range(len(AUTHORS)):
|
# Load or initialize state
|
||||||
author = AUTHORS[round_robin_index % len(AUTHORS)]
|
state = load_json_file(state_file, default={'last_author_index': -1})
|
||||||
round_robin_index = (round_robin_index + 1) % len(AUTHORS)
|
last_index = state.get('last_author_index', -1)
|
||||||
|
|
||||||
if not check_author_rate_limit(author):
|
# Try each author, starting from the next one after last_index
|
||||||
logger.info(f"Selected author via round-robin: {author['username']}")
|
for i in range(len(AUTHORS)):
|
||||||
|
index = (last_index + 1 + i) % len(AUTHORS)
|
||||||
|
author = AUTHORS[index]
|
||||||
|
username = author['username']
|
||||||
|
can_post, remaining, reset = check_author_rate_limit(author)
|
||||||
|
if can_post:
|
||||||
|
# Update state with the selected author index
|
||||||
|
state['last_author_index'] = index
|
||||||
|
save_json_file(state_file, state)
|
||||||
|
logger.info(f"Selected author {username} with {remaining}/17 tweets remaining")
|
||||||
return author
|
return author
|
||||||
|
else:
|
||||||
|
reset_time = datetime.fromtimestamp(reset, tz=timezone.utc).strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
logger.info(f"Author {username} is rate-limited. Remaining: {remaining}, Reset at: {reset_time}")
|
||||||
|
|
||||||
logger.warning("No authors available due to tweet rate limits.")
|
logger.warning("No authors available due to tweet rate limits.")
|
||||||
return None
|
return None
|
||||||
@@ -1278,7 +1280,7 @@ def get_x_rate_limit_status(author):
|
|||||||
logger.info(f"Rate limit exceeded for {username}: {remaining} remaining, reset at {datetime.fromtimestamp(reset, tz=timezone.utc)}")
|
logger.info(f"Rate limit exceeded for {username}: {remaining} remaining, reset at {datetime.fromtimestamp(reset, tz=timezone.utc)}")
|
||||||
elif response.status_code == 403:
|
elif response.status_code == 403:
|
||||||
# Forbidden (e.g., account restrictions), but headers may still provide rate limit info
|
# Forbidden (e.g., account restrictions), but headers may still provide rate limit info
|
||||||
logger.warning(f"403 Forbidden for {username}, but rate limit info available: {remaining} remaining, reset at {datetime.fromtimestamp(reset, tz=timezone.utc)}")
|
logger.warning(f"403 Forbidden for {username}: {response.text}, rate limit info: {remaining} remaining, reset at {datetime.fromtimestamp(reset, tz=timezone.utc)}")
|
||||||
else:
|
else:
|
||||||
logger.error(f"Unexpected response for {username}: {response.status_code} - {response.text}")
|
logger.error(f"Unexpected response for {username}: {response.status_code} - {response.text}")
|
||||||
return None, None
|
return None, None
|
||||||
|
|||||||
Reference in New Issue
Block a user