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
|
HTTP_TIMEOUT = 20
|
||||||
POST_RETRY_DELAY_SECONDS = 2
|
POST_RETRY_DELAY_SECONDS = 2
|
||||||
|
|
||||||
|
# Thumbnail upload cooldown state
|
||||||
THUMB_UPLOAD_COOLDOWN_UNTIL = 0
|
THUMB_UPLOAD_COOLDOWN_UNTIL = 0
|
||||||
|
|
||||||
|
# Post creation cooldown state
|
||||||
|
POST_CREATION_COOLDOWN_UNTIL = 0
|
||||||
|
|
||||||
# --- Logging ---
|
# --- Logging ---
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
format="%(asctime)s %(message)s",
|
format="%(asctime)s %(message)s",
|
||||||
@@ -83,7 +87,8 @@ def strip_trailing_url_punctuation(url):
|
|||||||
def canonicalize_url(url):
|
def canonicalize_url(url):
|
||||||
if not url:
|
if not url:
|
||||||
return None
|
return None
|
||||||
return strip_trailing_url_punctuation(url.strip())
|
url = html.unescape(url.strip())
|
||||||
|
return strip_trailing_url_punctuation(url)
|
||||||
|
|
||||||
|
|
||||||
def clean_whitespace(text):
|
def clean_whitespace(text):
|
||||||
@@ -137,12 +142,9 @@ def build_post_text_variants(title_text, link):
|
|||||||
1. Full title + blank line + real URL
|
1. Full title + blank line + real URL
|
||||||
Fallbacks:
|
Fallbacks:
|
||||||
2. Full title only
|
2. Full title only
|
||||||
3. Truncated title only
|
|
||||||
|
|
||||||
Intentionally avoid:
|
Intentionally avoid:
|
||||||
- truncated title + URL
|
- truncated title + URL
|
||||||
|
|
||||||
That pattern creates ugly posts with "..." before the visible link.
|
|
||||||
"""
|
"""
|
||||||
title_text = clean_whitespace(title_text)
|
title_text = clean_whitespace(title_text)
|
||||||
link = canonicalize_url(link) or link or ""
|
link = canonicalize_url(link) or link or ""
|
||||||
@@ -161,7 +163,6 @@ def build_post_text_variants(title_text, link):
|
|||||||
|
|
||||||
if title_text:
|
if title_text:
|
||||||
add_variant(title_text)
|
add_variant(title_text)
|
||||||
add_variant(truncate_text_safely(title_text))
|
|
||||||
|
|
||||||
if link and not title_text:
|
if link and not title_text:
|
||||||
add_variant(link)
|
add_variant(link)
|
||||||
@@ -454,7 +455,7 @@ def make_rich(content):
|
|||||||
return text_builder
|
return text_builder
|
||||||
|
|
||||||
|
|
||||||
# --- Blob / image upload helpers ---
|
# --- Rate limit helpers ---
|
||||||
def get_rate_limit_wait_seconds(error_obj, default_delay):
|
def get_rate_limit_wait_seconds(error_obj, default_delay):
|
||||||
try:
|
try:
|
||||||
headers = getattr(error_obj, "headers", None)
|
headers = getattr(error_obj, "headers", None)
|
||||||
@@ -483,6 +484,11 @@ def get_rate_limit_reset_timestamp(error_obj):
|
|||||||
return None
|
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):
|
def is_transient_blob_error(error_obj):
|
||||||
error_text = repr(error_obj)
|
error_text = repr(error_obj)
|
||||||
transient_signals = [
|
transient_signals = [
|
||||||
@@ -499,11 +505,30 @@ def is_transient_blob_error(error_obj):
|
|||||||
return any(signal in error_text for signal in transient_signals)
|
return any(signal in error_text for signal in transient_signals)
|
||||||
|
|
||||||
|
|
||||||
def is_rate_limited_error(error_obj):
|
# --- Post cooldown helpers ---
|
||||||
error_text = str(error_obj)
|
def activate_post_creation_cooldown_from_error(error_obj):
|
||||||
return "429" in error_text or "RateLimitExceeded" in error_text
|
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):
|
def activate_thumb_upload_cooldown_from_error(error_obj):
|
||||||
global THUMB_UPLOAD_COOLDOWN_UNTIL
|
global THUMB_UPLOAD_COOLDOWN_UNTIL
|
||||||
|
|
||||||
@@ -526,6 +551,7 @@ def is_thumb_upload_cooldown_active():
|
|||||||
return int(time.time()) < THUMB_UPLOAD_COOLDOWN_UNTIL
|
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):
|
def upload_blob_with_retry(client, binary_data, media_label="media", optional=False, cooldown_on_rate_limit=False):
|
||||||
last_exception = None
|
last_exception = None
|
||||||
transient_attempts = 0
|
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):
|
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
|
last_exception = None
|
||||||
|
|
||||||
for idx, variant in enumerate(text_variants, start=1):
|
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
|
last_exception = e
|
||||||
logging.warning(f"Post variant {idx} failed: {repr(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):
|
if not is_probable_length_error(e):
|
||||||
raise
|
raise
|
||||||
|
|
||||||
@@ -894,11 +930,17 @@ def main():
|
|||||||
|
|
||||||
with httpx.Client() as http_client:
|
with httpx.Client() as http_client:
|
||||||
for candidate in entries_to_post:
|
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"]
|
title_text = candidate["title_text"]
|
||||||
canonical_link = candidate["canonical_link"]
|
canonical_link = candidate["canonical_link"]
|
||||||
text_variants = candidate["post_text_variants"]
|
text_variants = candidate["post_text_variants"]
|
||||||
|
|
||||||
logging.info(f"Preparing to post RSS entry: {canonical_link or title_text}")
|
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
|
embed = None
|
||||||
if canonical_link:
|
if canonical_link:
|
||||||
@@ -933,11 +975,17 @@ def main():
|
|||||||
recent_bsky_posts = recent_bsky_posts[:DEDUPE_BSKY_LIMIT]
|
recent_bsky_posts = recent_bsky_posts[:DEDUPE_BSKY_LIMIT]
|
||||||
|
|
||||||
noves_entrades += 1
|
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}")
|
logging.info(f"Posted RSS entry to Bluesky: {canonical_link or title_text}")
|
||||||
time.sleep(POST_RETRY_DELAY_SECONDS)
|
time.sleep(POST_RETRY_DELAY_SECONDS)
|
||||||
|
|
||||||
except Exception:
|
except Exception as e:
|
||||||
logging.exception(f"Failed to post RSS entry {canonical_link or title_text}")
|
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:
|
if noves_entrades > 0:
|
||||||
logging.info(f"🎉 Execution finished: published {noves_entrades} new entries to Bluesky.")
|
logging.info(f"🎉 Execution finished: published {noves_entrades} new entries to Bluesky.")
|
||||||
@@ -946,4 +994,4 @@ def main():
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
Reference in New Issue
Block a user