feat(rss): improve external card thumbnail uploads with stronger compression and shorter cooldowns

This commit is contained in:
2026-04-13 09:47:28 +02:00
parent 3f50e7d786
commit 07bfd5e2d5

View File

@@ -31,21 +31,23 @@ DEFAULT_COOLDOWN_STATE_PATH = "rss2bsky_cooldowns.json"
DEDUPE_BSKY_LIMIT = 30
BSKY_TEXT_MAX_LENGTH = 275
EXTERNAL_THUMB_MAX_BYTES = 950 * 1024
EXTERNAL_THUMB_MAX_DIMENSION = 1200
EXTERNAL_THUMB_MIN_JPEG_QUALITY = 40
# External thumbnail tuning
EXTERNAL_THUMB_MAX_BYTES = 750 * 1024
EXTERNAL_THUMB_TARGET_BYTES = 500 * 1024
EXTERNAL_THUMB_MAX_DIMENSION = 1000
EXTERNAL_THUMB_MIN_JPEG_QUALITY = 35
BSKY_BLOB_UPLOAD_MAX_RETRIES = 5
BSKY_BLOB_UPLOAD_BASE_DELAY = 10
BSKY_BLOB_UPLOAD_MAX_DELAY = 300
BSKY_BLOB_TRANSIENT_ERROR_RETRIES = 3
BSKY_BLOB_TRANSIENT_ERROR_DELAY = 15
BSKY_BLOB_UPLOAD_MAX_RETRIES = 3
BSKY_BLOB_UPLOAD_BASE_DELAY = 8
BSKY_BLOB_UPLOAD_MAX_DELAY = 120
BSKY_BLOB_TRANSIENT_ERROR_RETRIES = 2
BSKY_BLOB_TRANSIENT_ERROR_DELAY = 10
HTTP_TIMEOUT = 20
POST_RETRY_DELAY_SECONDS = 2
DEFAULT_POST_COOLDOWN_SECONDS = 3600
DEFAULT_THUMB_COOLDOWN_SECONDS = 3600
DEFAULT_THUMB_COOLDOWN_SECONDS = 1800
# --- Logging ---
@@ -679,7 +681,7 @@ def upload_blob_with_retry(client, binary_data, media_label="media", optional=Fa
return None
def compress_external_thumb_to_limit(image_bytes, max_bytes=EXTERNAL_THUMB_MAX_BYTES):
def compress_external_thumb_to_limit(image_bytes, target_bytes=EXTERNAL_THUMB_TARGET_BYTES, hard_max_bytes=EXTERNAL_THUMB_MAX_BYTES):
if not PIL_AVAILABLE:
return None
@@ -694,15 +696,30 @@ def compress_external_thumb_to_limit(image_bytes, max_bytes=EXTERNAL_THUMB_MAX_B
scale = EXTERNAL_THUMB_MAX_DIMENSION / max_dim
new_size = (max(1, int(width * scale)), max(1, int(height * scale)))
img = img.resize(new_size, Image.LANCZOS)
logging.info(f"🖼️ Resized external thumb to {new_size[0]}x{new_size[1]}")
for quality in [85, 75, 65, 55, 45, EXTERNAL_THUMB_MIN_JPEG_QUALITY]:
for quality in [78, 70, 62, 54, 46, 40, EXTERNAL_THUMB_MIN_JPEG_QUALITY]:
out = io.BytesIO()
img.save(out, format="JPEG", quality=quality, optimize=True, progressive=True)
data = out.getvalue()
if len(data) <= max_bytes:
logging.info(
f"🖼️ External thumb candidate size at JPEG quality {quality}: "
f"{len(data) / 1024:.2f} KB"
)
if len(data) <= target_bytes:
return data
for target_dim in [1000, 900, 800, 700, 600]:
if len(data) <= hard_max_bytes:
best_so_far = data
# Additional downscale passes
best_candidate = locals().get("best_so_far")
if best_candidate and len(best_candidate) <= hard_max_bytes:
return best_candidate
for target_dim in [900, 800, 700, 600, 500]:
resized = img.copy()
width, height = resized.size
max_dim = max(width, height)
@@ -712,13 +729,25 @@ def compress_external_thumb_to_limit(image_bytes, max_bytes=EXTERNAL_THUMB_MAX_B
new_size = (max(1, int(width * scale)), max(1, int(height * scale)))
resized = resized.resize(new_size, Image.LANCZOS)
for quality in [60, 50, 45, EXTERNAL_THUMB_MIN_JPEG_QUALITY]:
for quality in [54, 46, 40, EXTERNAL_THUMB_MIN_JPEG_QUALITY]:
out = io.BytesIO()
resized.save(out, format="JPEG", quality=quality, optimize=True, progressive=True)
data = out.getvalue()
if len(data) <= max_bytes:
logging.info(
f"🖼️ External thumb resized to <= {target_dim}px at quality {quality}: "
f"{len(data) / 1024:.2f} KB"
)
if len(data) <= target_bytes:
return data
if len(data) <= hard_max_bytes:
best_candidate = data
if best_candidate and len(best_candidate) <= hard_max_bytes:
return best_candidate
except Exception as e:
logging.warning(f"⚠️ Could not compress external thumbnail: {repr(e)}")
@@ -742,16 +771,16 @@ def get_external_thumb_blob_from_url(image_url, client, http_client, cooldown_pa
logging.warning(f"⚠️ Could not fetch external thumb {image_url}: empty body")
return None
upload_bytes = content
if len(upload_bytes) > EXTERNAL_THUMB_MAX_BYTES:
compressed = compress_external_thumb_to_limit(upload_bytes, EXTERNAL_THUMB_MAX_BYTES)
if compressed:
upload_bytes = compressed
else:
logging.warning("⚠️ Could not compress external thumb to fit limit. Omitting thumbnail.")
logging.info(f"🖼️ Downloaded external thumb {image_url} ({len(content) / 1024:.2f} KB)")
upload_bytes = compress_external_thumb_to_limit(content)
if not upload_bytes:
logging.warning("⚠️ Could not prepare compressed external thumbnail. Omitting thumbnail.")
return None
return upload_blob_with_retry(
logging.info(f"🖼️ Final external thumb upload size: {len(upload_bytes) / 1024:.2f} KB")
blob = upload_blob_with_retry(
client,
upload_bytes,
media_label=f"external-thumb:{image_url}",
@@ -759,6 +788,12 @@ def get_external_thumb_blob_from_url(image_url, client, http_client, cooldown_pa
cooldown_on_rate_limit=True,
cooldown_path=cooldown_path
)
if blob:
logging.info("✅ External thumbnail uploaded successfully")
return blob
logging.warning("⚠️ External thumbnail upload failed. Will omit thumbnail.")
return None
except Exception as e:
logging.warning(f"⚠️ Could not fetch/upload external thumb {image_url}: {repr(e)}")
@@ -802,6 +837,8 @@ def build_external_link_embed(url, fallback_title, client, http_client, cooldown
logging.info("✅ External link card thumbnail prepared successfully")
else:
logging.info(" External link card will be posted without thumbnail")
else:
logging.info(" No og:image found for external link card")
if link_metadata.get("title") or link_metadata.get("description") or thumb_blob:
return models.AppBskyEmbedExternal.Main(
@@ -878,6 +915,7 @@ def is_probable_length_error(exc):
"string too long",
"maxLength",
"length",
"grapheme too big",
]
return any(signal.lower() in text.lower() for signal in signals)