@ -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 / / 6 0) } " # Cache for 1 minute
cache_key = f " { username } _ { int ( current_time / / 30 0) } " # 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