fixes
This commit is contained in:
191
bsky_post.py
191
bsky_post.py
@@ -2,12 +2,10 @@
|
|||||||
"""
|
"""
|
||||||
bsky_post.py — Post text + optional image or video to Bluesky/federated PDS.
|
bsky_post.py — Post text + optional image or video to Bluesky/federated PDS.
|
||||||
|
|
||||||
Usage examples:
|
Examples:
|
||||||
python3 bsky_post.py "DIVENDRES!!!!" --video friday.mp4 --username you --password app-pass --service https://eurosky.social
|
python3 bsky_post.py "DIVENDRES!!!!" --video media/divendres.mp4 --username you --password app-pass --service https://eurosky.social
|
||||||
python3 bsky_post.py "Dijous!!!!" --image thursday.jpg --username you --password app-pass
|
python3 bsky_post.py "Dijous!!!!" --image media/dijous.jpg --username you --password app-pass --service https://eurosky.social
|
||||||
python3 bsky_post.py "Bon dia!" --username you --password app-pass
|
python3 bsky_post.py "Bon dia!" --username you --password app-pass --service https://eurosky.social
|
||||||
python3 bsky_post.py "Bon dia!" --image photo.jpg --lang ca --username you --password app-pass
|
|
||||||
python3 bsky_post.py "Long video!" --video clip.mp4 --username you --password app-pass --allow-pds-video-fallback
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -22,7 +20,7 @@ from dataclasses import dataclass
|
|||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from atproto import Client, client_utils, models
|
from atproto import Client, models
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -47,47 +45,6 @@ def setup_logging() -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# Text builder
|
|
||||||
# ============================================================
|
|
||||||
def make_rich(content: str):
|
|
||||||
text_builder = client_utils.TextBuilder()
|
|
||||||
content = content.strip()
|
|
||||||
lines = content.splitlines()
|
|
||||||
|
|
||||||
for line_idx, line in enumerate(lines):
|
|
||||||
if not line.strip():
|
|
||||||
if line_idx < len(lines) - 1:
|
|
||||||
text_builder.text("\n")
|
|
||||||
continue
|
|
||||||
|
|
||||||
words = line.split(" ")
|
|
||||||
for i, word in enumerate(words):
|
|
||||||
if not word:
|
|
||||||
if i < len(words) - 1:
|
|
||||||
text_builder.text(" ")
|
|
||||||
continue
|
|
||||||
|
|
||||||
if word.startswith("http://") or word.startswith("https://"):
|
|
||||||
text_builder.link(word, word)
|
|
||||||
elif word.startswith("#") and len(word) > 1:
|
|
||||||
tag_name = word[1:].rstrip(".,;:!?)'\"")
|
|
||||||
if tag_name:
|
|
||||||
text_builder.tag(word, tag_name)
|
|
||||||
else:
|
|
||||||
text_builder.text(word)
|
|
||||||
else:
|
|
||||||
text_builder.text(word)
|
|
||||||
|
|
||||||
if i < len(words) - 1:
|
|
||||||
text_builder.text(" ")
|
|
||||||
|
|
||||||
if line_idx < len(lines) - 1:
|
|
||||||
text_builder.text("\n")
|
|
||||||
|
|
||||||
return text_builder
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Error helpers
|
# Error helpers
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -147,27 +104,7 @@ def is_transient_error(error_obj) -> bool:
|
|||||||
def get_rate_limit_wait_seconds(error_obj, default_delay: float, max_delay: float) -> float:
|
def get_rate_limit_wait_seconds(error_obj, default_delay: float, max_delay: float) -> float:
|
||||||
try:
|
try:
|
||||||
now_ts = int(time.time())
|
now_ts = int(time.time())
|
||||||
|
|
||||||
headers = getattr(error_obj, "headers", None) or {}
|
headers = getattr(error_obj, "headers", None) or {}
|
||||||
retry_after = headers.get("retry-after") or headers.get("Retry-After")
|
|
||||||
if retry_after:
|
|
||||||
return min(max(float(retry_after), 1.0), max_delay)
|
|
||||||
|
|
||||||
x_after = headers.get("x-ratelimit-after") or headers.get("X-RateLimit-After")
|
|
||||||
if x_after:
|
|
||||||
return min(max(float(x_after), 1.0), max_delay)
|
|
||||||
|
|
||||||
reset_value = headers.get("ratelimit-reset") or headers.get("RateLimit-Reset")
|
|
||||||
if reset_value:
|
|
||||||
wait_seconds = max(float(reset_value) - now_ts + 1.0, default_delay)
|
|
||||||
return min(wait_seconds, max_delay)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = getattr(error_obj, "response", None)
|
|
||||||
headers = getattr(response, "headers", None) or {}
|
|
||||||
now_ts = int(time.time())
|
|
||||||
|
|
||||||
retry_after = headers.get("retry-after") or headers.get("Retry-After")
|
retry_after = headers.get("retry-after") or headers.get("Retry-After")
|
||||||
if retry_after:
|
if retry_after:
|
||||||
@@ -232,36 +169,25 @@ def login_with_backoff(
|
|||||||
if is_rate_limited_error(e):
|
if is_rate_limited_error(e):
|
||||||
if attempt < max_attempts:
|
if attempt < max_attempts:
|
||||||
wait = get_rate_limit_wait_seconds(e, default_delay=base_delay, max_delay=max_delay)
|
wait = get_rate_limit_wait_seconds(e, default_delay=base_delay, max_delay=max_delay)
|
||||||
wait = wait + random.uniform(0, jitter)
|
wait += random.uniform(0, jitter)
|
||||||
logging.warning(
|
logging.warning(f"⏳ Rate-limited. Retrying in {wait:.1f}s...")
|
||||||
f"⏳ Rate-limited on login (attempt {attempt}/{max_attempts}). "
|
|
||||||
f"Retrying in {wait:.1f}s..."
|
|
||||||
)
|
|
||||||
time.sleep(wait)
|
time.sleep(wait)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
logging.error("❌ Exhausted login retries due to rate limiting.")
|
logging.error("❌ Exhausted login retries due to rate limiting.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if is_network_error(e) or is_transient_error(e):
|
if is_network_error(e) or is_transient_error(e):
|
||||||
if attempt < max_attempts:
|
if attempt < max_attempts:
|
||||||
wait = min(base_delay * attempt, max_delay) + random.uniform(0, jitter)
|
wait = min(base_delay * attempt, max_delay) + random.uniform(0, jitter)
|
||||||
logging.warning(
|
logging.warning(f"⏳ Transient error. Retrying in {wait:.1f}s...")
|
||||||
f"⏳ Transient login error (attempt {attempt}/{max_attempts}). "
|
|
||||||
f"Retrying in {wait:.1f}s..."
|
|
||||||
)
|
|
||||||
time.sleep(wait)
|
time.sleep(wait)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
logging.error("❌ Exhausted login retries after transient/network errors.")
|
logging.error("❌ Exhausted login retries after transient/network errors.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if attempt < max_attempts:
|
if attempt < max_attempts:
|
||||||
wait = min(base_delay * attempt, max_delay) + random.uniform(0, jitter)
|
wait = min(base_delay * attempt, max_delay) + random.uniform(0, jitter)
|
||||||
logging.warning(
|
logging.warning(f"⏳ Unknown login error. Retrying in {wait:.1f}s...")
|
||||||
f"⏳ Unknown login error (attempt {attempt}/{max_attempts}). "
|
|
||||||
f"Retrying in {wait:.1f}s..."
|
|
||||||
)
|
|
||||||
time.sleep(wait)
|
time.sleep(wait)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -289,10 +215,9 @@ def detect_mime_type(path: str) -> str:
|
|||||||
return fallbacks.get(ext, "application/octet-stream")
|
return fallbacks.get(ext, "application/octet-stream")
|
||||||
|
|
||||||
|
|
||||||
def wait_with_heartbeat(total_seconds: float, label: str = "indexing") -> None:
|
def wait_with_heartbeat(total_seconds: float, label: str = "processing") -> None:
|
||||||
if total_seconds <= 0:
|
if total_seconds <= 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
logging.info(f"⏳ Waiting {total_seconds:.0f}s for {label}...")
|
logging.info(f"⏳ Waiting {total_seconds:.0f}s for {label}...")
|
||||||
remaining = total_seconds
|
remaining = total_seconds
|
||||||
while remaining > 0:
|
while remaining > 0:
|
||||||
@@ -311,6 +236,16 @@ def pds_did_from_service_url(service_url: str) -> str:
|
|||||||
return f"did:web:{host}"
|
return f"did:web:{host}"
|
||||||
|
|
||||||
|
|
||||||
|
def model_to_dict(obj):
|
||||||
|
if obj is None:
|
||||||
|
return None
|
||||||
|
if hasattr(obj, "model_dump"):
|
||||||
|
return obj.model_dump(by_alias=True, exclude_none=True)
|
||||||
|
if hasattr(obj, "dict"):
|
||||||
|
return obj.dict(by_alias=True, exclude_none=True)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Media upload — Image
|
# Media upload — Image
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -320,6 +255,10 @@ def upload_image(
|
|||||||
alt_text: str = "",
|
alt_text: str = "",
|
||||||
) -> models.AppBskyEmbedImages.Image | None:
|
) -> models.AppBskyEmbedImages.Image | None:
|
||||||
try:
|
try:
|
||||||
|
if not os.path.exists(image_path):
|
||||||
|
logging.error(f"❌ Image file not found: {image_path}")
|
||||||
|
return None
|
||||||
|
|
||||||
mime = detect_mime_type(image_path)
|
mime = detect_mime_type(image_path)
|
||||||
with open(image_path, "rb") as f:
|
with open(image_path, "rb") as f:
|
||||||
data = f.read()
|
data = f.read()
|
||||||
@@ -336,7 +275,7 @@ def upload_image(
|
|||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Media upload — Video (PDS direct fallback only)
|
# Media upload — Video via PDS direct fallback
|
||||||
# ============================================================
|
# ============================================================
|
||||||
def upload_video_via_pds(
|
def upload_video_via_pds(
|
||||||
client: Client,
|
client: Client,
|
||||||
@@ -346,7 +285,7 @@ def upload_video_via_pds(
|
|||||||
) -> models.AppBskyEmbedVideo.Main | None:
|
) -> models.AppBskyEmbedVideo.Main | None:
|
||||||
"""
|
"""
|
||||||
Direct upload to home PDS via upload_blob.
|
Direct upload to home PDS via upload_blob.
|
||||||
Kept only as optional fallback; playback can be unreliable in clients.
|
Fallback only. Can be less reliable for playback in clients.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if not os.path.exists(video_path):
|
if not os.path.exists(video_path):
|
||||||
@@ -373,7 +312,7 @@ def upload_video_via_pds(
|
|||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Media upload — Video (video.bsky.app primary)
|
# Media upload — Video via video.bsky.app (primary)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
def _extract_service_auth_token(upload_auth) -> str | None:
|
def _extract_service_auth_token(upload_auth) -> str | None:
|
||||||
token = getattr(upload_auth, "token", None)
|
token = getattr(upload_auth, "token", None)
|
||||||
@@ -393,9 +332,9 @@ def upload_video_via_bsky_service(
|
|||||||
"""
|
"""
|
||||||
Upload via centralized video.bsky.app service.
|
Upload via centralized video.bsky.app service.
|
||||||
|
|
||||||
IMPORTANT FIX:
|
Critical compatibility fixes:
|
||||||
getServiceAuth(aud=...) must use the user's PDS DID (e.g. did:web:eurosky.social),
|
- aud must be user's PDS DID (e.g. did:web:eurosky.social)
|
||||||
not did:web:video.bsky.app.
|
- lxm must be com.atproto.repo.uploadBlob
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if not os.path.exists(video_path):
|
if not os.path.exists(video_path):
|
||||||
@@ -411,19 +350,19 @@ def upload_video_via_bsky_service(
|
|||||||
VIDEO_HOST = "https://video.bsky.app"
|
VIDEO_HOST = "https://video.bsky.app"
|
||||||
pds_did = pds_did_from_service_url(service_url)
|
pds_did = pds_did_from_service_url(service_url)
|
||||||
|
|
||||||
# Robust for different atproto versions
|
# Some atproto versions prefer typed params, others accept dict
|
||||||
try:
|
try:
|
||||||
params = models.ComAtprotoServerGetServiceAuth.Params(
|
params = models.ComAtprotoServerGetServiceAuth.Params(
|
||||||
aud=pds_did, # <-- critical fix
|
aud=pds_did,
|
||||||
lxm="app.bsky.video.uploadVideo",
|
lxm="com.atproto.repo.uploadBlob", # <-- critical fix
|
||||||
exp=int(time.time()) + 60 * 30,
|
exp=int(time.time()) + 60 * 30,
|
||||||
)
|
)
|
||||||
upload_auth = client.com.atproto.server.get_service_auth(params)
|
upload_auth = client.com.atproto.server.get_service_auth(params)
|
||||||
except Exception:
|
except Exception:
|
||||||
upload_auth = client.com.atproto.server.get_service_auth(
|
upload_auth = client.com.atproto.server.get_service_auth(
|
||||||
{
|
{
|
||||||
"aud": pds_did, # <-- critical fix
|
"aud": pds_did,
|
||||||
"lxm": "app.bsky.video.uploadVideo",
|
"lxm": "com.atproto.repo.uploadBlob", # <-- critical fix
|
||||||
"exp": int(time.time()) + 60 * 30,
|
"exp": int(time.time()) + 60 * 30,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -505,10 +444,7 @@ def upload_video_smart(
|
|||||||
settle_delay_seconds: float = 30.0,
|
settle_delay_seconds: float = 30.0,
|
||||||
allow_pds_video_fallback: bool = False,
|
allow_pds_video_fallback: bool = False,
|
||||||
) -> models.AppBskyEmbedVideo.Main | None:
|
) -> models.AppBskyEmbedVideo.Main | None:
|
||||||
logging.info(
|
logging.info(f"🌍 PDS ({service_url}). Trying video.bsky.app first.")
|
||||||
f"🌍 PDS ({service_url}). Trying video.bsky.app first for playback reliability."
|
|
||||||
)
|
|
||||||
|
|
||||||
embed = upload_video_via_bsky_service(
|
embed = upload_video_via_bsky_service(
|
||||||
client=client,
|
client=client,
|
||||||
video_path=video_path,
|
video_path=video_path,
|
||||||
@@ -519,10 +455,7 @@ def upload_video_smart(
|
|||||||
return embed
|
return embed
|
||||||
|
|
||||||
if allow_pds_video_fallback:
|
if allow_pds_video_fallback:
|
||||||
logging.warning(
|
logging.warning("⚠️ video.bsky.app failed; trying direct PDS fallback.")
|
||||||
"⚠️ video.bsky.app failed; trying direct PDS fallback "
|
|
||||||
"(may be unplayable in some clients)."
|
|
||||||
)
|
|
||||||
return upload_video_via_pds(
|
return upload_video_via_pds(
|
||||||
client=client,
|
client=client,
|
||||||
video_path=video_path,
|
video_path=video_path,
|
||||||
@@ -530,15 +463,12 @@ def upload_video_smart(
|
|||||||
settle_delay_seconds=settle_delay_seconds,
|
settle_delay_seconds=settle_delay_seconds,
|
||||||
)
|
)
|
||||||
|
|
||||||
logging.error(
|
logging.error("❌ video.bsky.app failed. Not using direct fallback unless enabled.")
|
||||||
"❌ video.bsky.app failed. Not posting unreliable direct-PDS video. "
|
|
||||||
"Use --allow-pds-video-fallback to override."
|
|
||||||
)
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Post
|
# Post creation (explicit record to guarantee text string)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
def post_to_bsky(
|
def post_to_bsky(
|
||||||
client: Client,
|
client: Client,
|
||||||
@@ -553,7 +483,6 @@ def post_to_bsky(
|
|||||||
) -> bool:
|
) -> bool:
|
||||||
post_text = text.strip()
|
post_text = text.strip()
|
||||||
|
|
||||||
# Allow empty text only if media exists
|
|
||||||
if not post_text and not image_path and not video_path:
|
if not post_text and not image_path and not video_path:
|
||||||
logging.error("❌ Empty post text with no media is not allowed.")
|
logging.error("❌ Empty post text with no media is not allowed.")
|
||||||
return False
|
return False
|
||||||
@@ -563,7 +492,7 @@ def post_to_bsky(
|
|||||||
|
|
||||||
if video_path:
|
if video_path:
|
||||||
logging.info(f"🎬 Preparing video upload: {video_path}")
|
logging.info(f"🎬 Preparing video upload: {video_path}")
|
||||||
video_embed = upload_video_smart(
|
embed_obj = upload_video_smart(
|
||||||
client=client,
|
client=client,
|
||||||
video_path=video_path,
|
video_path=video_path,
|
||||||
service_url=service_url,
|
service_url=service_url,
|
||||||
@@ -571,10 +500,9 @@ def post_to_bsky(
|
|||||||
settle_delay_seconds=video_settle_delay,
|
settle_delay_seconds=video_settle_delay,
|
||||||
allow_pds_video_fallback=allow_pds_video_fallback,
|
allow_pds_video_fallback=allow_pds_video_fallback,
|
||||||
)
|
)
|
||||||
if not video_embed:
|
if not embed_obj:
|
||||||
logging.error("❌ Aborting post: video upload/processing failed.")
|
logging.error("❌ Aborting post: video upload/processing failed.")
|
||||||
return False
|
return False
|
||||||
embed_obj = video_embed
|
|
||||||
|
|
||||||
elif image_path:
|
elif image_path:
|
||||||
image = upload_image(client, image_path, alt_text=alt_text)
|
image = upload_image(client, image_path, alt_text=alt_text)
|
||||||
@@ -583,10 +511,9 @@ def post_to_bsky(
|
|||||||
return False
|
return False
|
||||||
embed_obj = models.AppBskyEmbedImages.Main(images=[image])
|
embed_obj = models.AppBskyEmbedImages.Main(images=[image])
|
||||||
|
|
||||||
# Build record explicitly (most reliable)
|
|
||||||
record = {
|
record = {
|
||||||
"$type": "app.bsky.feed.post",
|
"$type": "app.bsky.feed.post",
|
||||||
"text": post_text, # <-- guaranteed string
|
"text": post_text, # guaranteed plain string
|
||||||
"createdAt": time.strftime("%Y-%m-%dT%H:%M:%S.000Z", time.gmtime()),
|
"createdAt": time.strftime("%Y-%m-%dT%H:%M:%S.000Z", time.gmtime()),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -594,16 +521,12 @@ def post_to_bsky(
|
|||||||
record["langs"] = langs
|
record["langs"] = langs
|
||||||
|
|
||||||
if embed_obj is not None:
|
if embed_obj is not None:
|
||||||
# atproto models -> plain dict
|
record["embed"] = model_to_dict(embed_obj)
|
||||||
if hasattr(embed_obj, "model_dump"):
|
|
||||||
record["embed"] = embed_obj.model_dump(by_alias=True, exclude_none=True)
|
|
||||||
elif hasattr(embed_obj, "dict"):
|
|
||||||
record["embed"] = embed_obj.dict(by_alias=True, exclude_none=True)
|
|
||||||
else:
|
|
||||||
record["embed"] = embed_obj
|
|
||||||
|
|
||||||
logging.info(f"🧾 Final record text={record.get('text')!r}, has_embed={'embed' in record}")
|
logging.info(f"🧾 Final record text={record.get('text')!r}, has_embed={'embed' in record}")
|
||||||
|
|
||||||
|
# typed first, dict fallback for compatibility
|
||||||
|
try:
|
||||||
resp = client.com.atproto.repo.create_record(
|
resp = client.com.atproto.repo.create_record(
|
||||||
models.ComAtprotoRepoCreateRecord.Data(
|
models.ComAtprotoRepoCreateRecord.Data(
|
||||||
repo=client.me.did,
|
repo=client.me.did,
|
||||||
@@ -611,6 +534,14 @@ def post_to_bsky(
|
|||||||
record=record,
|
record=record,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
except Exception:
|
||||||
|
resp = client.com.atproto.repo.create_record(
|
||||||
|
{
|
||||||
|
"repo": client.me.did,
|
||||||
|
"collection": "app.bsky.feed.post",
|
||||||
|
"record": record,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
uri = getattr(resp, "uri", None) or (resp.get("uri") if isinstance(resp, dict) else None)
|
uri = getattr(resp, "uri", None) or (resp.get("uri") if isinstance(resp, dict) else None)
|
||||||
logging.info(f"✅ Post published! URI: {uri}")
|
logging.info(f"✅ Post published! URI: {uri}")
|
||||||
@@ -620,33 +551,32 @@ def post_to_bsky(
|
|||||||
logging.error(f"❌ Failed to send post: {repr(e)}")
|
logging.error(f"❌ Failed to send post: {repr(e)}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# CLI
|
# CLI
|
||||||
# ============================================================
|
# ============================================================
|
||||||
def main():
|
def main():
|
||||||
setup_logging()
|
setup_logging()
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(description="Post text + optional image/video to Bluesky/federated PDS.")
|
||||||
description="Post text + optional image or video to Bluesky/federated PDS."
|
|
||||||
)
|
|
||||||
parser.add_argument("text", help="Post text content")
|
parser.add_argument("text", help="Post text content")
|
||||||
parser.add_argument("--username", required=True, help="Bluesky handle or email")
|
parser.add_argument("--username", required=True, help="Bluesky handle or email")
|
||||||
parser.add_argument("--password", required=True, help="Bluesky app password")
|
parser.add_argument("--password", required=True, help="Bluesky app password")
|
||||||
parser.add_argument("--service", default="https://bsky.social", help="Bluesky PDS URL")
|
parser.add_argument("--service", default="https://bsky.social", help="PDS URL")
|
||||||
parser.add_argument("--lang", default="ca", help="Comma-separated language codes (e.g. ca,es)")
|
parser.add_argument("--lang", default="ca", help="Comma-separated language codes (e.g. ca,es)")
|
||||||
parser.add_argument("--image", default=None, help="Path to image file to attach")
|
parser.add_argument("--image", default=None, help="Path to image file")
|
||||||
parser.add_argument("--video", default=None, help="Path to video file to attach")
|
parser.add_argument("--video", default=None, help="Path to video file")
|
||||||
parser.add_argument("--alt", default="", help="Alt text for media")
|
parser.add_argument("--alt", default="", help="Alt text for media")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--video-settle-delay",
|
"--video-settle-delay",
|
||||||
type=float,
|
type=float,
|
||||||
default=30.0,
|
default=30.0,
|
||||||
help="Seconds to wait after direct-PDS fallback upload before posting (default: 30).",
|
help="Seconds to wait after direct-PDS fallback upload before posting.",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--allow-pds-video-fallback",
|
"--allow-pds-video-fallback",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Allow direct-PDS fallback if video.bsky.app fails (less reliable playback).",
|
help="Allow direct PDS video fallback if video.bsky.app fails.",
|
||||||
)
|
)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
@@ -656,6 +586,7 @@ def main():
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
client = Client(base_url=args.service)
|
client = Client(base_url=args.service)
|
||||||
|
|
||||||
success = login_with_backoff(
|
success = login_with_backoff(
|
||||||
client=client,
|
client=client,
|
||||||
username=args.username,
|
username=args.username,
|
||||||
|
|||||||
Reference in New Issue
Block a user