fix(rss): stop posting after Bluesky createRecord rate limit and add clear post start/finish logs
This commit is contained in:
72
rss2bsky.py
72
rss2bsky.py
@@ -41,8 +41,12 @@ BSKY_BLOB_TRANSIENT_ERROR_DELAY = 15
|
||||
HTTP_TIMEOUT = 20
|
||||
POST_RETRY_DELAY_SECONDS = 2
|
||||
|
||||
# Thumbnail upload cooldown state
|
||||
THUMB_UPLOAD_COOLDOWN_UNTIL = 0
|
||||
|
||||
# Post creation cooldown state
|
||||
POST_CREATION_COOLDOWN_UNTIL = 0
|
||||
|
||||
# --- Logging ---
|
||||
logging.basicConfig(
|
||||
format="%(asctime)s %(message)s",
|
||||
@@ -83,7 +87,8 @@ def strip_trailing_url_punctuation(url):
|
||||
def canonicalize_url(url):
|
||||
if not url:
|
||||
return None
|
||||
return strip_trailing_url_punctuation(url.strip())
|
||||
url = html.unescape(url.strip())
|
||||
return strip_trailing_url_punctuation(url)
|
||||
|
||||
|
||||
def clean_whitespace(text):
|
||||
@@ -137,12 +142,9 @@ def build_post_text_variants(title_text, link):
|
||||
1. Full title + blank line + real URL
|
||||
Fallbacks:
|
||||
2. Full title only
|
||||
3. Truncated title only
|
||||
|
||||
Intentionally avoid:
|
||||
- truncated title + URL
|
||||
|
||||
That pattern creates ugly posts with "..." before the visible link.
|
||||
"""
|
||||
title_text = clean_whitespace(title_text)
|
||||
link = canonicalize_url(link) or link or ""
|
||||
@@ -161,7 +163,6 @@ def build_post_text_variants(title_text, link):
|
||||
|
||||
if title_text:
|
||||
add_variant(title_text)
|
||||
add_variant(truncate_text_safely(title_text))
|
||||
|
||||
if link and not title_text:
|
||||
add_variant(link)
|
||||
@@ -454,7 +455,7 @@ def make_rich(content):
|
||||
return text_builder
|
||||
|
||||
|
||||
# --- Blob / image upload helpers ---
|
||||
# --- Rate limit helpers ---
|
||||
def get_rate_limit_wait_seconds(error_obj, default_delay):
|
||||
try:
|
||||
headers = getattr(error_obj, "headers", None)
|
||||
@@ -483,6 +484,11 @@ def get_rate_limit_reset_timestamp(error_obj):
|
||||
return None
|
||||
|
||||
|
||||
def is_rate_limited_error(error_obj):
|
||||
error_text = str(error_obj)
|
||||
return "429" in error_text or "RateLimitExceeded" in error_text
|
||||
|
||||
|
||||
def is_transient_blob_error(error_obj):
|
||||
error_text = repr(error_obj)
|
||||
transient_signals = [
|
||||
@@ -499,11 +505,30 @@ def is_transient_blob_error(error_obj):
|
||||
return any(signal in error_text for signal in transient_signals)
|
||||
|
||||
|
||||
def is_rate_limited_error(error_obj):
|
||||
error_text = str(error_obj)
|
||||
return "429" in error_text or "RateLimitExceeded" in error_text
|
||||
# --- Post cooldown helpers ---
|
||||
def activate_post_creation_cooldown_from_error(error_obj):
|
||||
global POST_CREATION_COOLDOWN_UNTIL
|
||||
|
||||
reset_ts = get_rate_limit_reset_timestamp(error_obj)
|
||||
if reset_ts:
|
||||
if reset_ts > POST_CREATION_COOLDOWN_UNTIL:
|
||||
POST_CREATION_COOLDOWN_UNTIL = reset_ts
|
||||
logging.error(
|
||||
f"=== BSKY POST STOPPED: RATE LIMITED === Posting disabled until "
|
||||
f"{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(reset_ts))}"
|
||||
)
|
||||
else:
|
||||
fallback_reset = int(time.time()) + 3600
|
||||
if fallback_reset > POST_CREATION_COOLDOWN_UNTIL:
|
||||
POST_CREATION_COOLDOWN_UNTIL = fallback_reset
|
||||
logging.error("=== BSKY POST STOPPED: RATE LIMITED === Posting disabled for 1 hour.")
|
||||
|
||||
|
||||
def is_post_creation_cooldown_active():
|
||||
return int(time.time()) < POST_CREATION_COOLDOWN_UNTIL
|
||||
|
||||
|
||||
# --- Thumbnail cooldown helpers ---
|
||||
def activate_thumb_upload_cooldown_from_error(error_obj):
|
||||
global THUMB_UPLOAD_COOLDOWN_UNTIL
|
||||
|
||||
@@ -526,6 +551,7 @@ def is_thumb_upload_cooldown_active():
|
||||
return int(time.time()) < THUMB_UPLOAD_COOLDOWN_UNTIL
|
||||
|
||||
|
||||
# --- Blob / image upload helpers ---
|
||||
def upload_blob_with_retry(client, binary_data, media_label="media", optional=False, cooldown_on_rate_limit=False):
|
||||
last_exception = None
|
||||
transient_attempts = 0
|
||||
@@ -787,6 +813,12 @@ def is_probable_length_error(exc):
|
||||
|
||||
|
||||
def try_send_post_with_variants(client, text_variants, embed, post_lang):
|
||||
global POST_CREATION_COOLDOWN_UNTIL
|
||||
|
||||
if is_post_creation_cooldown_active():
|
||||
reset_str = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(POST_CREATION_COOLDOWN_UNTIL))
|
||||
raise RuntimeError(f"Posting skipped because post creation cooldown is active until {reset_str}")
|
||||
|
||||
last_exception = None
|
||||
|
||||
for idx, variant in enumerate(text_variants, start=1):
|
||||
@@ -800,6 +832,10 @@ def try_send_post_with_variants(client, text_variants, embed, post_lang):
|
||||
last_exception = e
|
||||
logging.warning(f"Post variant {idx} failed: {repr(e)}")
|
||||
|
||||
if is_rate_limited_error(e):
|
||||
activate_post_creation_cooldown_from_error(e)
|
||||
raise
|
||||
|
||||
if not is_probable_length_error(e):
|
||||
raise
|
||||
|
||||
@@ -894,11 +930,17 @@ def main():
|
||||
|
||||
with httpx.Client() as http_client:
|
||||
for candidate in entries_to_post:
|
||||
if is_post_creation_cooldown_active():
|
||||
reset_str = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(POST_CREATION_COOLDOWN_UNTIL))
|
||||
logging.error(f"=== BSKY POST STOPPED: RATE LIMITED === Skipping remaining entries until {reset_str}")
|
||||
break
|
||||
|
||||
title_text = candidate["title_text"]
|
||||
canonical_link = candidate["canonical_link"]
|
||||
text_variants = candidate["post_text_variants"]
|
||||
|
||||
logging.info(f"Preparing to post RSS entry: {canonical_link or title_text}")
|
||||
logging.info(f"=== BSKY POST START === {canonical_link or title_text}")
|
||||
|
||||
embed = None
|
||||
if canonical_link:
|
||||
@@ -933,11 +975,17 @@ def main():
|
||||
recent_bsky_posts = recent_bsky_posts[:DEDUPE_BSKY_LIMIT]
|
||||
|
||||
noves_entrades += 1
|
||||
logging.info(f"=== BSKY POST SUCCESS === {canonical_link or title_text}")
|
||||
logging.info(f"Posted RSS entry to Bluesky: {canonical_link or title_text}")
|
||||
time.sleep(POST_RETRY_DELAY_SECONDS)
|
||||
|
||||
except Exception:
|
||||
logging.exception(f"Failed to post RSS entry {canonical_link or title_text}")
|
||||
except Exception as e:
|
||||
logging.exception(f"=== BSKY POST FAILED === {canonical_link or title_text}")
|
||||
|
||||
if is_rate_limited_error(e) or is_post_creation_cooldown_active():
|
||||
reset_str = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(POST_CREATION_COOLDOWN_UNTIL))
|
||||
logging.error(f"=== BSKY POST STOPPED: RATE LIMITED === Ending publish loop until {reset_str}")
|
||||
break
|
||||
|
||||
if noves_entrades > 0:
|
||||
logging.info(f"🎉 Execution finished: published {noves_entrades} new entries to Bluesky.")
|
||||
@@ -946,4 +994,4 @@ def main():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user