fff
This commit is contained in:
92
bsky_post.py
92
bsky_post.py
@@ -7,15 +7,7 @@ Usage examples:
|
||||
python3 bsky_post.py "Dijous!!!!" --image thursday.jpg
|
||||
python3 bsky_post.py "Bon dia!"
|
||||
python3 bsky_post.py "Bon dia!" --image photo.jpg --lang ca
|
||||
|
||||
Notes:
|
||||
* Self-hosted / federated PDSes (e.g. eurosky.social) upload videos
|
||||
DIRECTLY to the user's PDS as a generic blob — no video.bsky.app
|
||||
service auth is required.
|
||||
* The official Bluesky network (bsky.social, *.bsky.network) uses the
|
||||
centralized video.bsky.app pipeline with a service-auth token scoped
|
||||
via `lxm` to `app.bsky.video.uploadVideo`.
|
||||
* The script auto-detects which path to use based on --service.
|
||||
python3 bsky_post.py "Long video!" --video clip.mp4 --video-settle-delay 25
|
||||
"""
|
||||
|
||||
import argparse
|
||||
@@ -153,9 +145,6 @@ def is_transient_error(error_obj) -> bool:
|
||||
|
||||
|
||||
def get_rate_limit_wait_seconds(error_obj, default_delay: float, max_delay: float) -> float:
|
||||
"""
|
||||
Parse common rate-limit headers and return a bounded wait time in seconds.
|
||||
"""
|
||||
try:
|
||||
now_ts = int(time.time())
|
||||
|
||||
@@ -341,6 +330,29 @@ def upload_image(
|
||||
return None
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Helpers — settle delay
|
||||
# ============================================================
|
||||
def wait_with_heartbeat(total_seconds: float, label: str = "indexing") -> None:
|
||||
"""
|
||||
Sleep `total_seconds` while logging a heartbeat every 5s so the operator
|
||||
can see we're still alive and how much time is left.
|
||||
"""
|
||||
if total_seconds <= 0:
|
||||
return
|
||||
|
||||
logging.info(f"⏳ Waiting {total_seconds:.0f}s for PDS {label}...")
|
||||
remaining = total_seconds
|
||||
while remaining > 0:
|
||||
step = min(5.0, remaining)
|
||||
time.sleep(step)
|
||||
remaining -= step
|
||||
if remaining > 0:
|
||||
logging.info(f" ...still waiting ({remaining:.0f}s remaining)...")
|
||||
|
||||
logging.info("✅ Settle delay complete.")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Media upload — Video (PDS-direct)
|
||||
# ============================================================
|
||||
@@ -348,6 +360,7 @@ def upload_video_via_pds(
|
||||
client: Client,
|
||||
video_path: str,
|
||||
alt_text: str = "",
|
||||
settle_delay_seconds: float = 15.0,
|
||||
) -> models.AppBskyEmbedVideo.Main | None:
|
||||
"""
|
||||
Upload a video as a generic blob directly to the user's PDS.
|
||||
@@ -355,6 +368,12 @@ def upload_video_via_pds(
|
||||
Used for self-hosted / federated PDSes (e.g. eurosky.social) that don't
|
||||
proxy to the centralized video.bsky.app service. The PDS stores the bytes
|
||||
and returns a blob ref we can embed directly.
|
||||
|
||||
A settle delay is applied AFTER the upload returns, because some PDSes
|
||||
(notably non-Bluesky-operated ones) index the blob asynchronously. If we
|
||||
publish the post immediately, the AppView can render "video not found"
|
||||
until indexing catches up — the delay ensures the post references a blob
|
||||
that's already retrievable.
|
||||
"""
|
||||
try:
|
||||
if not os.path.exists(video_path):
|
||||
@@ -365,11 +384,16 @@ def upload_video_via_pds(
|
||||
video_bytes = f.read()
|
||||
|
||||
size_mb = len(video_bytes) / (1024 * 1024)
|
||||
logging.info(f"🎬 [PDS-direct] Uploading video to home PDS: {video_path} ({size_mb:.2f} MB)")
|
||||
logging.info(
|
||||
f"🎬 [PDS-direct] Uploading video to home PDS: {video_path} ({size_mb:.2f} MB)"
|
||||
)
|
||||
|
||||
response = client.upload_blob(video_bytes)
|
||||
blob = response.blob
|
||||
logging.info(f"✅ [PDS-direct] Video blob uploaded successfully.")
|
||||
logging.info("✅ [PDS-direct] Video blob uploaded successfully.")
|
||||
|
||||
# Give PDS / AppView time to index the blob before we reference it.
|
||||
wait_with_heartbeat(settle_delay_seconds, label="to index the video blob")
|
||||
|
||||
return models.AppBskyEmbedVideo.Main(
|
||||
video=blob,
|
||||
@@ -402,12 +426,13 @@ def upload_video_via_bsky_service(
|
||||
video_bytes = f.read()
|
||||
|
||||
size_mb = len(video_bytes) / (1024 * 1024)
|
||||
logging.info(f"🎬 [video.bsky.app] Uploading via shared service: {video_path} ({size_mb:.2f} MB)")
|
||||
logging.info(
|
||||
f"🎬 [video.bsky.app] Uploading via shared service: {video_path} ({size_mb:.2f} MB)"
|
||||
)
|
||||
|
||||
VIDEO_HOST = "https://video.bsky.app"
|
||||
VIDEO_DID = "did:web:video.bsky.app"
|
||||
|
||||
# --- Service auth token (lxm-scoped → 30 min OK) ---
|
||||
upload_auth = client.com.atproto.server.get_service_auth({
|
||||
"aud": VIDEO_DID,
|
||||
"lxm": "app.bsky.video.uploadVideo",
|
||||
@@ -439,9 +464,8 @@ def upload_video_via_bsky_service(
|
||||
|
||||
logging.info(f"⏳ Job {job_id} accepted — polling status...")
|
||||
|
||||
# getJobStatus is unauthenticated on video.bsky.app
|
||||
status_url = f"{VIDEO_HOST}/xrpc/app.bsky.video.getJobStatus"
|
||||
deadline = time.time() + 300 # 5-minute ceiling
|
||||
deadline = time.time() + 300
|
||||
|
||||
while time.time() < deadline:
|
||||
status_resp = requests.get(status_url, params={"jobId": job_id}, timeout=30)
|
||||
@@ -493,14 +517,12 @@ def upload_video_smart(
|
||||
video_path: str,
|
||||
service_url: str,
|
||||
alt_text: str = "",
|
||||
settle_delay_seconds: float = 15.0,
|
||||
) -> models.AppBskyEmbedVideo.Main | None:
|
||||
"""
|
||||
Smart dispatcher: pick PDS-direct or video.bsky.app based on the PDS.
|
||||
|
||||
* Self-hosted / federated PDSes (eurosky.social etc.) → direct PDS upload
|
||||
(no service auth needed; PDS stores video itself).
|
||||
* Official Bluesky network (bsky.social, *.bsky.network) → video.bsky.app
|
||||
with lxm-scoped service auth, falling back to PDS-direct on failure.
|
||||
Smart dispatcher:
|
||||
* Self-hosted / federated PDSes → direct PDS upload (with settle delay)
|
||||
* Official Bluesky network → video.bsky.app, fallback to PDS-direct
|
||||
"""
|
||||
if is_official_bsky_pds(service_url):
|
||||
logging.info(f"🌐 Detected official Bluesky PDS ({service_url}) — using video.bsky.app")
|
||||
@@ -511,10 +533,16 @@ def upload_video_smart(
|
||||
logging.warning(
|
||||
"⚠️ video.bsky.app upload failed; falling back to direct PDS upload."
|
||||
)
|
||||
return upload_video_via_pds(client, video_path, alt_text=alt_text)
|
||||
return upload_video_via_pds(
|
||||
client, video_path, alt_text=alt_text,
|
||||
settle_delay_seconds=settle_delay_seconds,
|
||||
)
|
||||
|
||||
logging.info(f"🌍 Detected self-hosted/federated PDS ({service_url}) — using direct upload")
|
||||
return upload_video_via_pds(client, video_path, alt_text=alt_text)
|
||||
return upload_video_via_pds(
|
||||
client, video_path, alt_text=alt_text,
|
||||
settle_delay_seconds=settle_delay_seconds,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
@@ -528,11 +556,11 @@ def post_to_bsky(
|
||||
video_path: str | None = None,
|
||||
alt_text: str = "",
|
||||
service_url: str = "https://bsky.social",
|
||||
video_settle_delay: float = 15.0,
|
||||
) -> bool:
|
||||
rich_text = make_rich(text)
|
||||
|
||||
try:
|
||||
# --- VIDEO POSTING ---
|
||||
if video_path:
|
||||
logging.info(f"🎬 Preparing video upload: {video_path}")
|
||||
|
||||
@@ -541,6 +569,7 @@ def post_to_bsky(
|
||||
video_path,
|
||||
service_url=service_url,
|
||||
alt_text=alt_text,
|
||||
settle_delay_seconds=video_settle_delay,
|
||||
)
|
||||
|
||||
if not video_embed:
|
||||
@@ -554,7 +583,6 @@ def post_to_bsky(
|
||||
langs=langs,
|
||||
)
|
||||
|
||||
# --- IMAGE POSTING ---
|
||||
elif image_path:
|
||||
image = upload_image(client, image_path, alt_text=alt_text)
|
||||
if not image:
|
||||
@@ -565,7 +593,6 @@ def post_to_bsky(
|
||||
logging.info("🚀 Sending image post...")
|
||||
result = client.send_post(text=rich_text, embed=embed, langs=langs)
|
||||
|
||||
# --- TEXT ONLY POSTING ---
|
||||
else:
|
||||
logging.info("🚀 Sending text post...")
|
||||
result = client.send_post(text=rich_text, langs=langs)
|
||||
@@ -596,6 +623,12 @@ def main():
|
||||
parser.add_argument("--image", default=None, help="Path to image file to attach")
|
||||
parser.add_argument("--video", default=None, help="Path to video file to attach")
|
||||
parser.add_argument("--alt", default="", help="Alt text for media")
|
||||
parser.add_argument(
|
||||
"--video-settle-delay",
|
||||
type=float,
|
||||
default=15.0,
|
||||
help="Seconds to wait after PDS-direct video upload before posting (default: 15)",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -622,6 +655,7 @@ def main():
|
||||
video_path=args.video,
|
||||
alt_text=args.alt,
|
||||
service_url=args.service,
|
||||
video_settle_delay=args.video_settle_delay,
|
||||
)
|
||||
|
||||
if not post_success:
|
||||
|
||||
Reference in New Issue
Block a user