diff --git a/foodie_utils.py b/foodie_utils.py index 37359f7..7935c2a 100644 --- a/foodie_utils.py +++ b/foodie_utils.py @@ -1151,21 +1151,11 @@ def select_best_author(content, interest_score): logging.error(f"Error in select_best_author: {e}") 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): """ 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. - 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. """ 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 = {} 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: 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(): """ 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__) + state_file = '/home/shane/foodie_automator/author_state.json' - for _ in range(len(AUTHORS)): - author = AUTHORS[round_robin_index % len(AUTHORS)] - round_robin_index = (round_robin_index + 1) % len(AUTHORS) - - if not check_author_rate_limit(author): - logger.info(f"Selected author via round-robin: {author['username']}") + # Load or initialize state + state = load_json_file(state_file, default={'last_author_index': -1}) + last_index = state.get('last_author_index', -1) + + # Try each author, starting from the next one after last_index + 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 + 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.") 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)}") elif response.status_code == 403: # 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: logger.error(f"Unexpected response for {username}: {response.status_code} - {response.text}") return None, None