update realtime rate limit for X
This commit is contained in:
+49
-17
@@ -24,10 +24,12 @@ from foodie_config import (
|
|||||||
)
|
)
|
||||||
from foodie_utils import (
|
from foodie_utils import (
|
||||||
load_json_file, save_json_file, get_image, generate_image_query,
|
load_json_file, save_json_file, get_image, generate_image_query,
|
||||||
upload_image_to_wp, select_best_persona, determine_paragraph_count,
|
upload_image_to_wp, determine_paragraph_count, insert_link_naturally,
|
||||||
is_interesting, generate_title_from_summary, summarize_with_gpt4o,
|
is_interesting, generate_title_from_summary, summarize_with_gpt4o,
|
||||||
generate_category_from_summary, post_to_wp, prepare_post_data,
|
generate_category_from_summary, post_to_wp, prepare_post_data,
|
||||||
smart_image_and_filter, insert_link_naturally, get_flickr_image
|
select_best_author, smart_image_and_filter, get_flickr_image,
|
||||||
|
get_next_author_round_robin, fetch_duckduckgo_news_context,
|
||||||
|
check_author_rate_limit
|
||||||
)
|
)
|
||||||
from foodie_hooks import get_dynamic_hook, get_viral_share_prompt
|
from foodie_hooks import get_dynamic_hook, get_viral_share_prompt
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
@@ -246,47 +248,61 @@ def fetch_duckduckgo_news_context(trend_title, hours=24):
|
|||||||
logging.error(f"Failed to fetch DuckDuckGo News context for '{trend_title}' after {MAX_RETRIES} attempts")
|
logging.error(f"Failed to fetch DuckDuckGo News context for '{trend_title}' after {MAX_RETRIES} attempts")
|
||||||
return trend_title
|
return trend_title
|
||||||
|
|
||||||
def curate_from_google_trends(geo_list=['US']):
|
def curate_from_google_trends():
|
||||||
try:
|
try:
|
||||||
all_trends = []
|
global posted_titles_data, posted_titles, used_images
|
||||||
for geo in geo_list:
|
posted_titles_data = load_json_file(POSTED_TITLES_FILE, EXPIRATION_HOURS)
|
||||||
trends = scrape_google_trends(geo=geo)
|
posted_titles = set(entry["title"] for entry in posted_titles_data)
|
||||||
if trends:
|
used_images = set(entry["title"] for entry in load_json_file(USED_IMAGES_FILE, IMAGE_EXPIRATION_DAYS) if "title" in entry)
|
||||||
all_trends.extend(trends)
|
logging.debug(f"Loaded {len(posted_titles)} posted titles and {len(used_images)} used images")
|
||||||
|
|
||||||
if not all_trends:
|
trends = fetch_google_trends()
|
||||||
|
if not trends:
|
||||||
|
print("No Google Trends data available")
|
||||||
logging.info("No Google Trends data available")
|
logging.info("No Google Trends data available")
|
||||||
return None, None, False
|
return None, None, False
|
||||||
|
|
||||||
attempts = 0
|
attempts = 0
|
||||||
max_attempts = 10
|
max_attempts = 10
|
||||||
while attempts < max_attempts and all_trends:
|
while attempts < max_attempts and trends:
|
||||||
trend = all_trends.pop(0)
|
trend = trends.pop(0)
|
||||||
title = trend["title"]
|
title = trend["title"]
|
||||||
link = trend.get("link", "https://trends.google.com/")
|
link = trend.get("link", "")
|
||||||
summary = trend.get("summary", "")
|
summary = trend.get("summary", "")
|
||||||
source_name = "Google Trends"
|
source_name = trend.get("source", "Google Trends")
|
||||||
original_source = f'<a href="{link}">{source_name}</a>'
|
original_source = f'<a href="{link}">{source_name}</a>'
|
||||||
|
|
||||||
if title in posted_titles:
|
if title in posted_titles:
|
||||||
|
print(f"Skipping already posted trend: {title}")
|
||||||
logging.info(f"Skipping already posted trend: {title}")
|
logging.info(f"Skipping already posted trend: {title}")
|
||||||
attempts += 1
|
attempts += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
print(f"Trying Google Trend: {title} from {source_name}")
|
||||||
logging.info(f"Trying Google Trend: {title} from {source_name}")
|
logging.info(f"Trying Google Trend: {title} from {source_name}")
|
||||||
|
|
||||||
image_query, relevance_keywords, main_topic, skip = smart_image_and_filter(title, summary)
|
try:
|
||||||
|
image_query, relevance_keywords, main_topic, skip = smart_image_and_filter(title, summary)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Smart image/filter error for '{title}': {e}")
|
||||||
|
logging.warning(f"Failed to process smart_image_and_filter for '{title}': {e}")
|
||||||
|
attempts += 1
|
||||||
|
continue
|
||||||
|
|
||||||
if skip:
|
if skip:
|
||||||
logging.info(f"Skipping filtered Google Trend: {title}")
|
print(f"Skipping filtered trend: {title}")
|
||||||
|
logging.info(f"Skipping filtered trend: {title}")
|
||||||
attempts += 1
|
attempts += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
ddg_context = fetch_duckduckgo_news_context(title)
|
ddg_context = fetch_duckduckgo_news_context(title)
|
||||||
scoring_content = f"{title}\n\n{summary}\n\nAdditional Context: {ddg_context}"
|
scoring_content = f"{title}\n\n{summary}\n\nAdditional Context: {ddg_context}"
|
||||||
interest_score = is_interesting(scoring_content)
|
interest_score = is_interesting(scoring_content)
|
||||||
|
print(f"Interest Score for '{title[:50]}...': {interest_score}")
|
||||||
logging.info(f"Interest score for '{title}': {interest_score}")
|
logging.info(f"Interest score for '{title}': {interest_score}")
|
||||||
if interest_score < 6:
|
if interest_score < 6:
|
||||||
logging.info(f"Google Trends Interest Too Low: {interest_score}")
|
print(f"Trend Interest Too Low: {interest_score}")
|
||||||
|
logging.info(f"Trend Interest Too Low: {interest_score}")
|
||||||
attempts += 1
|
attempts += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -308,6 +324,7 @@ def curate_from_google_trends(geo_list=['US']):
|
|||||||
extra_prompt=extra_prompt
|
extra_prompt=extra_prompt
|
||||||
)
|
)
|
||||||
if not final_summary:
|
if not final_summary:
|
||||||
|
print(f"Summary failed for '{title}'")
|
||||||
logging.info(f"Summary failed for '{title}'")
|
logging.info(f"Summary failed for '{title}'")
|
||||||
attempts += 1
|
attempts += 1
|
||||||
continue
|
continue
|
||||||
@@ -329,15 +346,17 @@ def curate_from_google_trends(geo_list=['US']):
|
|||||||
category = post_data["categories"][0]
|
category = post_data["categories"][0]
|
||||||
image_url, image_source, uploader, page_url = get_flickr_image(image_query, relevance_keywords, main_topic)
|
image_url, image_source, uploader, page_url = get_flickr_image(image_query, relevance_keywords, main_topic)
|
||||||
if not image_url:
|
if not image_url:
|
||||||
|
print(f"Flickr image fetch failed for '{image_query}', trying fallback")
|
||||||
|
logging.warning(f"Flickr image fetch failed for '{image_query}', trying fallback")
|
||||||
image_url, image_source, uploader, page_url = get_image(image_query)
|
image_url, image_source, uploader, page_url = get_image(image_query)
|
||||||
if not image_url:
|
if not image_url:
|
||||||
|
print(f"All image uploads failed for '{title}' - posting without image")
|
||||||
logging.warning(f"All image uploads failed for '{title}' - posting without image")
|
logging.warning(f"All image uploads failed for '{title}' - posting without image")
|
||||||
image_source = None
|
image_source = None
|
||||||
uploader = None
|
uploader = None
|
||||||
page_url = None
|
page_url = None
|
||||||
|
|
||||||
hook = get_dynamic_hook(post_data["title"]).strip()
|
hook = get_dynamic_hook(post_data["title"]).strip()
|
||||||
|
|
||||||
share_prompt = get_viral_share_prompt(post_data["title"], final_summary)
|
share_prompt = get_viral_share_prompt(post_data["title"], final_summary)
|
||||||
share_links_template = (
|
share_links_template = (
|
||||||
f'<p>{share_prompt} '
|
f'<p>{share_prompt} '
|
||||||
@@ -362,7 +381,13 @@ def curate_from_google_trends(geo_list=['US']):
|
|||||||
interest_score=interest_score,
|
interest_score=interest_score,
|
||||||
should_post_tweet=True
|
should_post_tweet=True
|
||||||
)
|
)
|
||||||
|
if not post_id:
|
||||||
|
print(f"Failed to post to WordPress for '{title}'")
|
||||||
|
logging.warning(f"Failed to post to WordPress for '{title}'")
|
||||||
|
attempts += 1
|
||||||
|
continue
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
print(f"WordPress posting error for '{title}': {e}")
|
||||||
logging.error(f"Failed to post to WordPress for '{title}': {e}", exc_info=True)
|
logging.error(f"Failed to post to WordPress for '{title}': {e}", exc_info=True)
|
||||||
attempts += 1
|
attempts += 1
|
||||||
continue
|
continue
|
||||||
@@ -392,6 +417,7 @@ def curate_from_google_trends(geo_list=['US']):
|
|||||||
should_post_tweet=False
|
should_post_tweet=False
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
print(f"Failed to update WordPress post '{title}' with share links: {e}")
|
||||||
logging.error(f"Failed to update WordPress post '{title}' with share links: {e}", exc_info=True)
|
logging.error(f"Failed to update WordPress post '{title}' with share links: {e}", exc_info=True)
|
||||||
finally:
|
finally:
|
||||||
is_posting = False
|
is_posting = False
|
||||||
@@ -399,23 +425,29 @@ def curate_from_google_trends(geo_list=['US']):
|
|||||||
timestamp = datetime.now(timezone.utc).isoformat()
|
timestamp = datetime.now(timezone.utc).isoformat()
|
||||||
save_json_file(POSTED_TITLES_FILE, title, timestamp)
|
save_json_file(POSTED_TITLES_FILE, title, timestamp)
|
||||||
posted_titles.add(title)
|
posted_titles.add(title)
|
||||||
|
print(f"Successfully saved '{title}' to {POSTED_TITLES_FILE}")
|
||||||
logging.info(f"Successfully saved '{title}' to {POSTED_TITLES_FILE}")
|
logging.info(f"Successfully saved '{title}' to {POSTED_TITLES_FILE}")
|
||||||
|
|
||||||
if image_url:
|
if image_url:
|
||||||
save_json_file(USED_IMAGES_FILE, image_url, timestamp)
|
save_json_file(USED_IMAGES_FILE, image_url, timestamp)
|
||||||
used_images.add(image_url)
|
used_images.add(image_url)
|
||||||
|
print(f"Saved image '{image_url}' to {USED_IMAGES_FILE}")
|
||||||
logging.info(f"Saved image '{image_url}' to {USED_IMAGES_FILE}")
|
logging.info(f"Saved image '{image_url}' to {USED_IMAGES_FILE}")
|
||||||
|
|
||||||
|
print(f"***** SUCCESS: Posted '{post_data['title']}' (ID: {post_id}) from Google Trends *****")
|
||||||
logging.info(f"***** SUCCESS: Posted '{post_data['title']}' (ID: {post_id}) from Google Trends *****")
|
logging.info(f"***** SUCCESS: Posted '{post_data['title']}' (ID: {post_id}) from Google Trends *****")
|
||||||
return post_data, category, True
|
return post_data, category, True
|
||||||
|
|
||||||
attempts += 1
|
attempts += 1
|
||||||
|
print(f"WP posting failed for '{post_data['title']}'")
|
||||||
logging.info(f"WP posting failed for '{post_data['title']}'")
|
logging.info(f"WP posting failed for '{post_data['title']}'")
|
||||||
|
|
||||||
|
print("No interesting Google Trend found after attempts")
|
||||||
logging.info("No interesting Google Trend found after attempts")
|
logging.info("No interesting Google Trend found after attempts")
|
||||||
return None, None, False
|
return None, None, False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Unexpected error in curate_from_google_trends: {e}", exc_info=True)
|
logging.error(f"Unexpected error in curate_from_google_trends: {e}", exc_info=True)
|
||||||
|
print(f"Unexpected error in curate_from_google_trends: {e}")
|
||||||
return None, None, False
|
return None, None, False
|
||||||
|
|
||||||
def run_google_trends_automator():
|
def run_google_trends_automator():
|
||||||
|
|||||||
+64
-43
@@ -25,9 +25,11 @@ from foodie_config import (
|
|||||||
from foodie_utils import (
|
from foodie_utils import (
|
||||||
load_json_file, save_json_file, get_image, generate_image_query,
|
load_json_file, save_json_file, get_image, generate_image_query,
|
||||||
upload_image_to_wp, determine_paragraph_count, insert_link_naturally,
|
upload_image_to_wp, determine_paragraph_count, insert_link_naturally,
|
||||||
summarize_with_gpt4o, generate_category_from_summary, post_to_wp,
|
is_interesting, generate_title_from_summary, summarize_with_gpt4o,
|
||||||
prepare_post_data, select_best_author, smart_image_and_filter,
|
generate_category_from_summary, post_to_wp, prepare_post_data,
|
||||||
get_flickr_image
|
select_best_author, smart_image_and_filter, get_flickr_image,
|
||||||
|
get_next_author_round_robin, fetch_duckduckgo_news_context,
|
||||||
|
check_author_rate_limit
|
||||||
)
|
)
|
||||||
from foodie_hooks import get_dynamic_hook, get_viral_share_prompt
|
from foodie_hooks import get_dynamic_hook, get_viral_share_prompt
|
||||||
import fcntl
|
import fcntl
|
||||||
@@ -268,55 +270,58 @@ def fetch_reddit_posts():
|
|||||||
|
|
||||||
def curate_from_reddit():
|
def curate_from_reddit():
|
||||||
try:
|
try:
|
||||||
articles = fetch_reddit_posts()
|
global posted_titles_data, posted_titles, used_images
|
||||||
if not articles:
|
posted_titles_data = load_json_file(POSTED_TITLES_FILE, EXPIRATION_HOURS)
|
||||||
|
posted_titles = set(entry["title"] for entry in posted_titles_data)
|
||||||
|
used_images = set(entry["title"] for entry in load_json_file(USED_IMAGES_FILE, IMAGE_EXPIRATION_DAYS) if "title" in entry)
|
||||||
|
logging.debug(f"Loaded {len(posted_titles)} posted titles and {len(used_images)} used images")
|
||||||
|
|
||||||
|
posts = fetch_reddit_posts()
|
||||||
|
if not posts:
|
||||||
|
print("No Reddit posts available")
|
||||||
logging.info("No Reddit posts available")
|
logging.info("No Reddit posts available")
|
||||||
return None, None, False
|
return None, None, False
|
||||||
|
|
||||||
articles.sort(key=lambda x: x["upvotes"], reverse=True)
|
|
||||||
|
|
||||||
reddit = praw.Reddit(
|
|
||||||
client_id=REDDIT_CLIENT_ID,
|
|
||||||
client_secret=REDDIT_CLIENT_SECRET,
|
|
||||||
user_agent=REDDIT_USER_AGENT
|
|
||||||
)
|
|
||||||
|
|
||||||
attempts = 0
|
attempts = 0
|
||||||
max_attempts = 10
|
max_attempts = 10
|
||||||
while attempts < max_attempts and articles:
|
while attempts < max_attempts and posts:
|
||||||
article = articles.pop(0)
|
post = posts.pop(0)
|
||||||
title = article["title"]
|
title = post["title"]
|
||||||
raw_title = article["raw_title"]
|
link = post.get("link", "")
|
||||||
link = article["link"]
|
summary = post.get("summary", "")
|
||||||
summary = article["summary"]
|
source_name = post.get("source", "Reddit")
|
||||||
source_name = "Reddit"
|
original_source = f'<a href="{link}">{source_name}</a>'
|
||||||
original_source = '<a href="https://www.reddit.com/">Reddit</a>'
|
|
||||||
|
|
||||||
if raw_title in posted_titles:
|
if title in posted_titles:
|
||||||
logging.info(f"Skipping already posted post: {raw_title}")
|
print(f"Skipping already posted Reddit post: {title}")
|
||||||
|
logging.info(f"Skipping already posted Reddit post: {title}")
|
||||||
attempts += 1
|
attempts += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
print(f"Trying Reddit Post: {title} from {source_name}")
|
||||||
logging.info(f"Trying Reddit Post: {title} from {source_name}")
|
logging.info(f"Trying Reddit Post: {title} from {source_name}")
|
||||||
|
|
||||||
image_query, relevance_keywords, main_topic, skip = smart_image_and_filter(title, summary)
|
try:
|
||||||
if skip or any(keyword in title.lower() or keyword in raw_title.lower() for keyword in RECIPE_KEYWORDS + ["homemade"]):
|
image_query, relevance_keywords, main_topic, skip = smart_image_and_filter(title, summary)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Smart image/filter error for '{title}': {e}")
|
||||||
|
logging.warning(f"Failed to process smart_image_and_filter for '{title}': {e}")
|
||||||
|
attempts += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if skip:
|
||||||
|
print(f"Skipping filtered Reddit post: {title}")
|
||||||
logging.info(f"Skipping filtered Reddit post: {title}")
|
logging.info(f"Skipping filtered Reddit post: {title}")
|
||||||
attempts += 1
|
attempts += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
top_comments = get_top_comments(link, reddit, limit=3)
|
|
||||||
ddg_context = fetch_duckduckgo_news_context(title)
|
ddg_context = fetch_duckduckgo_news_context(title)
|
||||||
content_to_summarize = f"{title}\n\n{summary}\n\nTop Comments:\n{'\n'.join(top_comments) if top_comments else 'None'}\n\nAdditional Context: {ddg_context}"
|
scoring_content = f"{title}\n\n{summary}\n\nAdditional Context: {ddg_context}"
|
||||||
interest_score = is_interesting_reddit(
|
interest_score = is_interesting(scoring_content)
|
||||||
title,
|
print(f"Interest Score for '{title[:50]}...': {interest_score}")
|
||||||
summary,
|
logging.info(f"Interest score for '{title}': {interest_score}")
|
||||||
article["upvotes"],
|
|
||||||
article["comment_count"],
|
|
||||||
top_comments
|
|
||||||
)
|
|
||||||
logging.info(f"Interest Score: {interest_score} for '{title}'")
|
|
||||||
if interest_score < 6:
|
if interest_score < 6:
|
||||||
|
print(f"Reddit Interest Too Low: {interest_score}")
|
||||||
logging.info(f"Reddit Interest Too Low: {interest_score}")
|
logging.info(f"Reddit Interest Too Low: {interest_score}")
|
||||||
attempts += 1
|
attempts += 1
|
||||||
continue
|
continue
|
||||||
@@ -325,13 +330,12 @@ def curate_from_reddit():
|
|||||||
extra_prompt = (
|
extra_prompt = (
|
||||||
f"Generate exactly {num_paragraphs} paragraphs.\n"
|
f"Generate exactly {num_paragraphs} paragraphs.\n"
|
||||||
f"FOCUS: Summarize ONLY the provided content, focusing on its specific topic and details without mentioning the original title.\n"
|
f"FOCUS: Summarize ONLY the provided content, focusing on its specific topic and details without mentioning the original title.\n"
|
||||||
f"Incorporate relevant insights from these top comments if available: {', '.join(top_comments) if top_comments else 'None'}.\n"
|
|
||||||
f"Incorporate relevant insights from this additional context if available: {ddg_context}.\n"
|
f"Incorporate relevant insights from this additional context if available: {ddg_context}.\n"
|
||||||
f"Do NOT introduce unrelated concepts unless in the content, comments, or additional context.\n"
|
f"Do NOT introduce unrelated concepts unless in the content or additional context.\n"
|
||||||
f"If brief, expand on the core idea with relevant context about its appeal or significance.\n"
|
f"Expand on the core idea with relevant context about its appeal or significance in food trends.\n"
|
||||||
f"Do not include emojis in the summary."
|
f"Do not include emojis in the summary."
|
||||||
)
|
)
|
||||||
|
content_to_summarize = scoring_content
|
||||||
final_summary = summarize_with_gpt4o(
|
final_summary = summarize_with_gpt4o(
|
||||||
content_to_summarize,
|
content_to_summarize,
|
||||||
source_name,
|
source_name,
|
||||||
@@ -340,6 +344,7 @@ def curate_from_reddit():
|
|||||||
extra_prompt=extra_prompt
|
extra_prompt=extra_prompt
|
||||||
)
|
)
|
||||||
if not final_summary:
|
if not final_summary:
|
||||||
|
print(f"Summary failed for '{title}'")
|
||||||
logging.info(f"Summary failed for '{title}'")
|
logging.info(f"Summary failed for '{title}'")
|
||||||
attempts += 1
|
attempts += 1
|
||||||
continue
|
continue
|
||||||
@@ -361,15 +366,17 @@ def curate_from_reddit():
|
|||||||
category = post_data["categories"][0]
|
category = post_data["categories"][0]
|
||||||
image_url, image_source, uploader, page_url = get_flickr_image(image_query, relevance_keywords, main_topic)
|
image_url, image_source, uploader, page_url = get_flickr_image(image_query, relevance_keywords, main_topic)
|
||||||
if not image_url:
|
if not image_url:
|
||||||
|
print(f"Flickr image fetch failed for '{image_query}', trying fallback")
|
||||||
|
logging.warning(f"Flickr image fetch failed for '{image_query}', trying fallback")
|
||||||
image_url, image_source, uploader, page_url = get_image(image_query)
|
image_url, image_source, uploader, page_url = get_image(image_query)
|
||||||
if not image_url:
|
if not image_url:
|
||||||
|
print(f"All image uploads failed for '{title}' - posting without image")
|
||||||
logging.warning(f"All image uploads failed for '{title}' - posting without image")
|
logging.warning(f"All image uploads failed for '{title}' - posting without image")
|
||||||
image_source = None
|
image_source = None
|
||||||
uploader = None
|
uploader = None
|
||||||
page_url = None
|
page_url = None
|
||||||
|
|
||||||
hook = get_dynamic_hook(post_data["title"]).strip()
|
hook = get_dynamic_hook(post_data["title"]).strip()
|
||||||
|
|
||||||
share_prompt = get_viral_share_prompt(post_data["title"], final_summary)
|
share_prompt = get_viral_share_prompt(post_data["title"], final_summary)
|
||||||
share_links_template = (
|
share_links_template = (
|
||||||
f'<p>{share_prompt} '
|
f'<p>{share_prompt} '
|
||||||
@@ -394,7 +401,13 @@ def curate_from_reddit():
|
|||||||
interest_score=interest_score,
|
interest_score=interest_score,
|
||||||
should_post_tweet=True
|
should_post_tweet=True
|
||||||
)
|
)
|
||||||
|
if not post_id:
|
||||||
|
print(f"Failed to post to WordPress for '{title}'")
|
||||||
|
logging.warning(f"Failed to post to WordPress for '{title}'")
|
||||||
|
attempts += 1
|
||||||
|
continue
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
print(f"WordPress posting error for '{title}': {e}")
|
||||||
logging.error(f"Failed to post to WordPress for '{title}': {e}", exc_info=True)
|
logging.error(f"Failed to post to WordPress for '{title}': {e}", exc_info=True)
|
||||||
attempts += 1
|
attempts += 1
|
||||||
continue
|
continue
|
||||||
@@ -424,29 +437,37 @@ def curate_from_reddit():
|
|||||||
should_post_tweet=False
|
should_post_tweet=False
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
print(f"Failed to update WordPress post '{title}' with share links: {e}")
|
||||||
logging.error(f"Failed to update WordPress post '{title}' with share links: {e}", exc_info=True)
|
logging.error(f"Failed to update WordPress post '{title}' with share links: {e}", exc_info=True)
|
||||||
finally:
|
finally:
|
||||||
is_posting = False
|
is_posting = False
|
||||||
|
|
||||||
timestamp = datetime.now(timezone.utc).isoformat()
|
timestamp = datetime.now(timezone.utc).isoformat()
|
||||||
save_json_file(POSTED_TITLES_FILE, raw_title, timestamp)
|
save_json_file(POSTED_TITLES_FILE, title, timestamp)
|
||||||
posted_titles.add(raw_title)
|
posted_titles.add(title)
|
||||||
logging.info(f"Successfully saved '{raw_title}' to {POSTED_TITLES_FILE}")
|
print(f"Successfully saved '{title}' to {POSTED_TITLES_FILE}")
|
||||||
|
logging.info(f"Successfully saved '{title}' to {POSTED_TITLES_FILE}")
|
||||||
|
|
||||||
if image_url:
|
if image_url:
|
||||||
save_json_file(USED_IMAGES_FILE, image_url, timestamp)
|
save_json_file(USED_IMAGES_FILE, image_url, timestamp)
|
||||||
used_images.add(image_url)
|
used_images.add(image_url)
|
||||||
|
print(f"Saved image '{image_url}' to {USED_IMAGES_FILE}")
|
||||||
logging.info(f"Saved image '{image_url}' to {USED_IMAGES_FILE}")
|
logging.info(f"Saved image '{image_url}' to {USED_IMAGES_FILE}")
|
||||||
|
|
||||||
|
print(f"***** SUCCESS: Posted '{post_data['title']}' (ID: {post_id}) from Reddit *****")
|
||||||
logging.info(f"***** SUCCESS: Posted '{post_data['title']}' (ID: {post_id}) from Reddit *****")
|
logging.info(f"***** SUCCESS: Posted '{post_data['title']}' (ID: {post_id}) from Reddit *****")
|
||||||
return post_data, category, True
|
return post_data, category, True
|
||||||
|
|
||||||
attempts += 1
|
attempts += 1
|
||||||
|
print(f"WP posting failed for '{post_data['title']}'")
|
||||||
logging.info(f"WP posting failed for '{post_data['title']}'")
|
logging.info(f"WP posting failed for '{post_data['title']}'")
|
||||||
|
|
||||||
|
print("No interesting Reddit post found after attempts")
|
||||||
logging.info("No interesting Reddit post found after attempts")
|
logging.info("No interesting Reddit post found after attempts")
|
||||||
return None, None, False
|
return None, None, False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Unexpected error in curate_from_reddit: {e}", exc_info=True)
|
logging.error(f"Unexpected error in curate_from_reddit: {e}", exc_info=True)
|
||||||
|
print(f"Unexpected error in curate_from_reddit: {e}")
|
||||||
return None, None, False
|
return None, None, False
|
||||||
|
|
||||||
def run_reddit_automator():
|
def run_reddit_automator():
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ from foodie_utils import (
|
|||||||
is_interesting, generate_title_from_summary, summarize_with_gpt4o,
|
is_interesting, generate_title_from_summary, summarize_with_gpt4o,
|
||||||
generate_category_from_summary, post_to_wp, prepare_post_data,
|
generate_category_from_summary, post_to_wp, prepare_post_data,
|
||||||
select_best_author, smart_image_and_filter, get_flickr_image,
|
select_best_author, smart_image_and_filter, get_flickr_image,
|
||||||
get_next_author_round_robin # Add this line
|
get_next_author_round_robin, check_author_rate_limit
|
||||||
)
|
)
|
||||||
from foodie_hooks import get_dynamic_hook, get_viral_share_prompt
|
from foodie_hooks import get_dynamic_hook, get_viral_share_prompt
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import fcntl
|
|||||||
import os
|
import os
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from openai import OpenAI
|
from openai import OpenAI
|
||||||
from foodie_utils import post_tweet, AUTHORS, SUMMARY_MODEL, load_post_counts, save_post_counts
|
from foodie_utils import post_tweet, AUTHORS, SUMMARY_MODEL, check_author_rate_limit
|
||||||
from foodie_config import X_API_CREDENTIALS, AUTHOR_BACKGROUNDS_FILE
|
from foodie_config import X_API_CREDENTIALS, AUTHOR_BACKGROUNDS_FILE
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
@@ -161,23 +161,15 @@ def post_engagement_tweet():
|
|||||||
logging.info("Starting foodie_engagement_tweet.py")
|
logging.info("Starting foodie_engagement_tweet.py")
|
||||||
print("Starting foodie_engagement_tweet.py")
|
print("Starting foodie_engagement_tweet.py")
|
||||||
|
|
||||||
# Load post counts to check limits
|
|
||||||
post_counts = load_post_counts()
|
|
||||||
|
|
||||||
for author in AUTHORS:
|
for author in AUTHORS:
|
||||||
try:
|
# Check if the author can post before generating the tweet
|
||||||
# Check post limits
|
can_post, remaining, reset = check_author_rate_limit(author)
|
||||||
author_count = next((entry for entry in post_counts if entry["username"] == author["username"]), None)
|
if not can_post:
|
||||||
if not author_count:
|
reset_time = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(reset)) if reset else "Unknown"
|
||||||
logging.error(f"No post count entry for {author['username']}, skipping")
|
logging.info(f"Skipping engagement tweet for {author['username']} due to rate limit. Remaining: {remaining}, Reset at: {reset_time}")
|
||||||
continue
|
continue
|
||||||
if author_count["monthly_count"] >= 500:
|
|
||||||
logging.warning(f"Monthly post limit (500) reached for {author['username']}, skipping")
|
|
||||||
continue
|
|
||||||
if author_count["daily_count"] >= 15:
|
|
||||||
logging.warning(f"Daily post limit (15) reached for {author['username']}, skipping")
|
|
||||||
continue
|
|
||||||
|
|
||||||
|
try:
|
||||||
tweet = generate_engagement_tweet(author)
|
tweet = generate_engagement_tweet(author)
|
||||||
if not tweet:
|
if not tweet:
|
||||||
logging.error(f"Failed to generate engagement tweet for {author['username']}, skipping")
|
logging.error(f"Failed to generate engagement tweet for {author['username']}, skipping")
|
||||||
@@ -187,10 +179,6 @@ def post_engagement_tweet():
|
|||||||
print(f"Posting engagement tweet for {author['username']}: {tweet}")
|
print(f"Posting engagement tweet for {author['username']}: {tweet}")
|
||||||
if post_tweet(author, tweet):
|
if post_tweet(author, tweet):
|
||||||
logging.info(f"Successfully posted engagement tweet for {author['username']}")
|
logging.info(f"Successfully posted engagement tweet for {author['username']}")
|
||||||
# Update post counts
|
|
||||||
author_count["monthly_count"] += 1
|
|
||||||
author_count["daily_count"] += 1
|
|
||||||
save_post_counts(post_counts)
|
|
||||||
else:
|
else:
|
||||||
logging.warning(f"Failed to post engagement tweet for {author['username']}")
|
logging.warning(f"Failed to post engagement tweet for {author['username']}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
+61
-28
@@ -199,7 +199,6 @@ def post_tweet(author, tweet, reply_to_id=None):
|
|||||||
from foodie_config import X_API_CREDENTIALS
|
from foodie_config import X_API_CREDENTIALS
|
||||||
import logging
|
import logging
|
||||||
import tweepy
|
import tweepy
|
||||||
from datetime import datetime, timezone
|
|
||||||
|
|
||||||
credentials = X_API_CREDENTIALS.get(author["username"])
|
credentials = X_API_CREDENTIALS.get(author["username"])
|
||||||
if not credentials:
|
if not credentials:
|
||||||
@@ -212,15 +211,6 @@ def post_tweet(author, tweet, reply_to_id=None):
|
|||||||
if reply_to_id:
|
if reply_to_id:
|
||||||
logging.debug(f"Replying to tweet ID: {reply_to_id}")
|
logging.debug(f"Replying to tweet ID: {reply_to_id}")
|
||||||
|
|
||||||
post_counts = load_post_counts()
|
|
||||||
author_count = next((entry for entry in post_counts if entry["username"] == author["username"]), None)
|
|
||||||
if author_count["monthly_count"] >= 500:
|
|
||||||
logging.warning(f"Monthly post limit (500) reached for {author['username']}")
|
|
||||||
return False
|
|
||||||
if author_count["daily_count"] >= 15: # Updated daily limit
|
|
||||||
logging.warning(f"Daily post limit (15) reached for {author['username']}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
client = tweepy.Client(
|
client = tweepy.Client(
|
||||||
consumer_key=credentials["api_key"],
|
consumer_key=credentials["api_key"],
|
||||||
@@ -232,9 +222,6 @@ def post_tweet(author, tweet, reply_to_id=None):
|
|||||||
text=tweet,
|
text=tweet,
|
||||||
in_reply_to_tweet_id=reply_to_id
|
in_reply_to_tweet_id=reply_to_id
|
||||||
)
|
)
|
||||||
author_count["monthly_count"] += 1
|
|
||||||
author_count["daily_count"] += 1
|
|
||||||
save_post_counts(post_counts)
|
|
||||||
logging.info(f"Posted tweet for {author['username']} (handle: {credentials['x_username']}): {tweet}")
|
logging.info(f"Posted tweet for {author['username']} (handle: {credentials['x_username']}): {tweet}")
|
||||||
logging.debug(f"Tweet ID: {response.data['id']}")
|
logging.debug(f"Tweet ID: {response.data['id']}")
|
||||||
return {"id": response.data["id"]}
|
return {"id": response.data["id"]}
|
||||||
@@ -1170,11 +1157,61 @@ 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(list(PERSONA_CONFIGS.keys()))
|
return random.choice(list(PERSONA_CONFIGS.keys()))
|
||||||
|
|
||||||
|
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):
|
||||||
|
"""Check the rate limit for a specific author by making a lightweight API call."""
|
||||||
|
credentials = X_API_CREDENTIALS.get(author["username"])
|
||||||
|
if not credentials:
|
||||||
|
logging.error(f"No X credentials found for {author['username']}")
|
||||||
|
return False, None, None
|
||||||
|
|
||||||
|
try:
|
||||||
|
client = tweepy.Client(
|
||||||
|
consumer_key=credentials["api_key"],
|
||||||
|
consumer_secret=credentials["api_secret"],
|
||||||
|
access_token=credentials["access_token"],
|
||||||
|
access_token_secret=credentials["access_token_secret"],
|
||||||
|
return_type=dict
|
||||||
|
)
|
||||||
|
# Use a lightweight endpoint to check rate limits (e.g., /users/me)
|
||||||
|
response = client.get_me()
|
||||||
|
remaining, reset = check_rate_limit(response)
|
||||||
|
if remaining is None or reset is None:
|
||||||
|
logging.warning(f"Could not determine rate limit for {author['username']}. Assuming rate limit is not hit.")
|
||||||
|
return True, None, None
|
||||||
|
if remaining <= 0:
|
||||||
|
reset_time = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(reset))
|
||||||
|
logging.info(f"Author {author['username']} is rate-limited. Remaining: {remaining}, Reset at: {reset_time}")
|
||||||
|
return False, remaining, reset
|
||||||
|
logging.debug(f"Author {author['username']} can post. Remaining: {remaining}, Reset at: {time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(reset))}")
|
||||||
|
return True, remaining, reset
|
||||||
|
except tweepy.TweepyException as e:
|
||||||
|
logging.error(f"Failed to check rate limit for {author['username']}: {e}")
|
||||||
|
if e.response and e.response.status_code == 429:
|
||||||
|
remaining, reset = check_rate_limit(e.response)
|
||||||
|
if remaining is not None and reset is not None:
|
||||||
|
reset_time = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(reset))
|
||||||
|
logging.info(f"Author {author['username']} is rate-limited. Remaining: {remaining}, Reset at: {reset_time}")
|
||||||
|
return False, remaining, reset
|
||||||
|
logging.warning(f"Assuming {author['username']} is rate-limited due to error.")
|
||||||
|
return False, None, None
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Unexpected error checking rate limit for {author['username']}: {e}", exc_info=True)
|
||||||
|
return False, None, None
|
||||||
|
|
||||||
def get_next_author_round_robin():
|
def get_next_author_round_robin():
|
||||||
"""Select the next author in a round-robin fashion, respecting daily tweet limits."""
|
"""Select the next author in a round-robin fashion, ensuring they are not rate-limited."""
|
||||||
last_author_file = "/home/shane/foodie_automator/last_author.json"
|
last_author_file = "/home/shane/foodie_automator/last_author.json"
|
||||||
authors = [author["username"] for author in AUTHORS]
|
authors = [author["username"] for author in AUTHORS]
|
||||||
post_counts = load_post_counts()
|
|
||||||
|
|
||||||
# Load the last used author
|
# Load the last used author
|
||||||
try:
|
try:
|
||||||
@@ -1188,20 +1225,17 @@ def get_next_author_round_robin():
|
|||||||
logging.warning(f"Failed to load last author from {last_author_file}: {e}. Starting from first author.")
|
logging.warning(f"Failed to load last author from {last_author_file}: {e}. Starting from first author.")
|
||||||
last_index = -1
|
last_index = -1
|
||||||
|
|
||||||
# Find the next author who hasn't reached the daily limit
|
# Find the next author who is not rate-limited
|
||||||
start_index = (last_index + 1) % len(authors)
|
start_index = (last_index + 1) % len(authors)
|
||||||
for i in range(len(authors)):
|
for i in range(len(authors)):
|
||||||
current_index = (start_index + i) % len(authors)
|
current_index = (start_index + i) % len(authors)
|
||||||
username = authors[current_index]
|
username = authors[current_index]
|
||||||
author_count = next((entry for entry in post_counts if entry["username"] == username), None)
|
author = next(author for author in AUTHORS if author["username"] == username)
|
||||||
if not author_count:
|
|
||||||
logging.error(f"No post count entry for {username}, skipping")
|
# Check if the author can post based on rate limits
|
||||||
continue
|
can_post, remaining, reset = check_author_rate_limit(author)
|
||||||
if author_count["daily_count"] >= 15: # Updated daily limit
|
if not can_post:
|
||||||
logging.info(f"Author {username} has reached daily limit ({author_count['daily_count']}/15), skipping")
|
logging.info(f"Skipping author {username} due to rate limit.")
|
||||||
continue
|
|
||||||
if author_count["monthly_count"] >= 500:
|
|
||||||
logging.info(f"Author {username} has reached monthly limit ({author_count['monthly_count']}/500), skipping")
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Save the current index as the last used author
|
# Save the current index as the last used author
|
||||||
@@ -1212,10 +1246,9 @@ def get_next_author_round_robin():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.warning(f"Failed to save last author to {last_author_file}: {e}")
|
logging.warning(f"Failed to save last author to {last_author_file}: {e}")
|
||||||
|
|
||||||
# Return the selected author
|
return author
|
||||||
return next(author for author in AUTHORS if author["username"] == username)
|
|
||||||
|
|
||||||
logging.warning("No authors available within daily/monthly limits. Selecting a random author as fallback.")
|
logging.warning("No authors available due to rate limits. Selecting a random author as fallback.")
|
||||||
return random.choice(AUTHORS)
|
return random.choice(AUTHORS)
|
||||||
|
|
||||||
def prepare_post_data(summary, title, main_topic=None):
|
def prepare_post_data(summary, title, main_topic=None):
|
||||||
|
|||||||
+6
-17
@@ -10,7 +10,7 @@ import time
|
|||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
import tweepy
|
import tweepy
|
||||||
from openai import OpenAI
|
from openai import OpenAI
|
||||||
from foodie_utils import post_tweet, AUTHORS, SUMMARY_MODEL, load_json_file
|
from foodie_utils import post_tweet, AUTHORS, SUMMARY_MODEL, load_json_file, check_author_rate_limit
|
||||||
from foodie_config import X_API_CREDENTIALS, RECENT_POSTS_FILE
|
from foodie_config import X_API_CREDENTIALS, RECENT_POSTS_FILE
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
@@ -310,9 +310,6 @@ def post_weekly_thread():
|
|||||||
if username in posts_by_author:
|
if username in posts_by_author:
|
||||||
posts_by_author[username].append(post)
|
posts_by_author[username].append(post)
|
||||||
|
|
||||||
# Load post counts to check limits
|
|
||||||
post_counts = load_post_counts()
|
|
||||||
|
|
||||||
# Post threads for each author
|
# Post threads for each author
|
||||||
for author in AUTHORS:
|
for author in AUTHORS:
|
||||||
username = author["username"]
|
username = author["username"]
|
||||||
@@ -321,19 +318,11 @@ def post_weekly_thread():
|
|||||||
logging.info(f"No posts found for {username}, skipping")
|
logging.info(f"No posts found for {username}, skipping")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check daily limit (each thread will use 3 tweets: lead + 2 thread tweets)
|
# Check if the author can post before generating the thread
|
||||||
author_count = next((entry for entry in post_counts if entry["username"] == username), None)
|
can_post, remaining, reset = check_author_rate_limit(author)
|
||||||
if not author_count:
|
if not can_post:
|
||||||
logging.error(f"No post count entry for {username}, skipping")
|
reset_time = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(reset)) if reset else "Unknown"
|
||||||
continue
|
logging.info(f"Skipping weekly thread for {username} due to rate limit. Remaining: {remaining}, Reset at: {reset_time}")
|
||||||
if author_count["daily_count"] >= 15:
|
|
||||||
logging.warning(f"Daily post limit (15) reached for {username}, skipping")
|
|
||||||
continue
|
|
||||||
if author_count["daily_count"] + 3 > 15:
|
|
||||||
logging.warning(f"Posting thread for {username} would exceed daily limit (current: {author_count['daily_count']}, needed: 3), skipping")
|
|
||||||
continue
|
|
||||||
if author_count["monthly_count"] >= 500:
|
|
||||||
logging.warning(f"Monthly post limit (500) reached for {username}, skipping")
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Select top 2 posts (to fit within 3-tweet limit: lead + 2 posts)
|
# Select top 2 posts (to fit within 3-tweet limit: lead + 2 posts)
|
||||||
|
|||||||
+18
-4
@@ -9,7 +9,7 @@ import os
|
|||||||
from datetime import datetime, timezone, timedelta
|
from datetime import datetime, timezone, timedelta
|
||||||
from openai import OpenAI
|
from openai import OpenAI
|
||||||
from foodie_config import OPENAI_API_KEY, AUTHORS, LIGHT_TASK_MODEL, PERSONA_CONFIGS, AUTHOR_BACKGROUNDS_FILE
|
from foodie_config import OPENAI_API_KEY, AUTHORS, LIGHT_TASK_MODEL, PERSONA_CONFIGS, AUTHOR_BACKGROUNDS_FILE
|
||||||
from foodie_utils import load_json_file, post_tweet
|
from foodie_utils import load_json_file, post_tweet, check_author_rate_limit
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
@@ -99,10 +99,24 @@ def main():
|
|||||||
global is_posting
|
global is_posting
|
||||||
logging.info("***** X Poster Launched *****")
|
logging.info("***** X Poster Launched *****")
|
||||||
for author in AUTHORS:
|
for author in AUTHORS:
|
||||||
|
# Check if the author can post before generating the tweet
|
||||||
|
can_post, remaining, reset = check_author_rate_limit(author)
|
||||||
|
if not can_post:
|
||||||
|
reset_time = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(reset)) if reset else "Unknown"
|
||||||
|
logging.info(f"Skipping engagement tweet for {author['username']} due to rate limit. Remaining: {remaining}, Reset at: {reset_time}")
|
||||||
|
continue
|
||||||
|
|
||||||
is_posting = True
|
is_posting = True
|
||||||
tweet = generate_engagement_tweet(author, author["persona"])
|
try:
|
||||||
post_tweet(author, tweet)
|
tweet = generate_engagement_tweet(author, author["persona"])
|
||||||
is_posting = False
|
if post_tweet(author, tweet):
|
||||||
|
logging.info(f"Successfully posted engagement tweet for {author['username']}")
|
||||||
|
else:
|
||||||
|
logging.warning(f"Failed to post engagement tweet for {author['username']}")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error posting engagement tweet for {author['username']}: {e}", exc_info=True)
|
||||||
|
finally:
|
||||||
|
is_posting = False
|
||||||
time.sleep(random.uniform(3600, 7200))
|
time.sleep(random.uniform(3600, 7200))
|
||||||
|
|
||||||
logging.info("X posting completed")
|
logging.info("X posting completed")
|
||||||
|
|||||||
Reference in New Issue
Block a user