fixes
This commit is contained in:
179
bsky_post.py
179
bsky_post.py
@@ -14,6 +14,8 @@ import mimetypes
|
|||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
|
import secrets
|
||||||
|
import string
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
@@ -236,6 +238,11 @@ def pds_did_from_service_url(service_url: str) -> str:
|
|||||||
return f"did:web:{host}"
|
return f"did:web:{host}"
|
||||||
|
|
||||||
|
|
||||||
|
def random_video_name(ext: str = ".mp4") -> str:
|
||||||
|
token = "".join(secrets.choice(string.ascii_lowercase + string.digits) for _ in range(12))
|
||||||
|
return f"{int(time.time())}_{token}{ext}"
|
||||||
|
|
||||||
|
|
||||||
def model_to_dict(obj):
|
def model_to_dict(obj):
|
||||||
if obj is None:
|
if obj is None:
|
||||||
return None
|
return None
|
||||||
@@ -323,79 +330,9 @@ def _extract_service_auth_token(upload_auth) -> str | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def upload_video_via_bsky_service(
|
def _poll_video_job(video_host: str, job_id: str) -> models.AppBskyEmbedVideo.Main | None:
|
||||||
client: Client,
|
status_url = f"{video_host}/xrpc/app.bsky.video.getJobStatus"
|
||||||
video_path: str,
|
deadline = time.time() + 600 # up to 10 minutes
|
||||||
service_url: str,
|
|
||||||
alt_text: str = "",
|
|
||||||
) -> models.AppBskyEmbedVideo.Main | None:
|
|
||||||
"""
|
|
||||||
Upload via centralized video.bsky.app service.
|
|
||||||
|
|
||||||
Critical compatibility fixes:
|
|
||||||
- aud must be user's PDS DID (e.g. did:web:eurosky.social)
|
|
||||||
- lxm must be com.atproto.repo.uploadBlob
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
if not os.path.exists(video_path):
|
|
||||||
logging.error(f"❌ Video file not found: {video_path}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
with open(video_path, "rb") as f:
|
|
||||||
video_bytes = f.read()
|
|
||||||
|
|
||||||
size_mb = len(video_bytes) / (1024 * 1024)
|
|
||||||
logging.info(f"🎬 [video.bsky.app] Uploading: {video_path} ({size_mb:.2f} MB)")
|
|
||||||
|
|
||||||
VIDEO_HOST = "https://video.bsky.app"
|
|
||||||
pds_did = pds_did_from_service_url(service_url)
|
|
||||||
|
|
||||||
# Some atproto versions prefer typed params, others accept dict
|
|
||||||
try:
|
|
||||||
params = models.ComAtprotoServerGetServiceAuth.Params(
|
|
||||||
aud=pds_did,
|
|
||||||
lxm="com.atproto.repo.uploadBlob", # <-- critical fix
|
|
||||||
exp=int(time.time()) + 60 * 30,
|
|
||||||
)
|
|
||||||
upload_auth = client.com.atproto.server.get_service_auth(params)
|
|
||||||
except Exception:
|
|
||||||
upload_auth = client.com.atproto.server.get_service_auth(
|
|
||||||
{
|
|
||||||
"aud": pds_did,
|
|
||||||
"lxm": "com.atproto.repo.uploadBlob", # <-- critical fix
|
|
||||||
"exp": int(time.time()) + 60 * 30,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
token = _extract_service_auth_token(upload_auth)
|
|
||||||
if not token:
|
|
||||||
logging.error("❌ Failed to extract service auth token.")
|
|
||||||
return None
|
|
||||||
|
|
||||||
user_did = client.me.did
|
|
||||||
upload_url = (
|
|
||||||
f"{VIDEO_HOST}/xrpc/app.bsky.video.uploadVideo"
|
|
||||||
f"?did={user_did}&name={int(time.time())}.mp4"
|
|
||||||
)
|
|
||||||
headers = {
|
|
||||||
"Authorization": f"Bearer {token}",
|
|
||||||
"Content-Type": "video/mp4",
|
|
||||||
}
|
|
||||||
|
|
||||||
upload_resp = requests.post(upload_url, headers=headers, data=video_bytes, timeout=180)
|
|
||||||
if upload_resp.status_code != 200:
|
|
||||||
logging.error(f"❌ video.bsky.app upload failed: {upload_resp.status_code} - {upload_resp.text}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
body = upload_resp.json()
|
|
||||||
job_id = body.get("jobId")
|
|
||||||
if not job_id:
|
|
||||||
logging.error(f"❌ No jobId returned from video service. Response: {body}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
logging.info(f"⏳ Job {job_id} accepted — polling status...")
|
|
||||||
status_url = f"{VIDEO_HOST}/xrpc/app.bsky.video.getJobStatus"
|
|
||||||
deadline = time.time() + 600
|
|
||||||
|
|
||||||
while time.time() < deadline:
|
while time.time() < deadline:
|
||||||
status_resp = requests.get(status_url, params={"jobId": job_id}, timeout=30)
|
status_resp = requests.get(status_url, params={"jobId": job_id}, timeout=30)
|
||||||
@@ -416,7 +353,7 @@ def upload_video_via_bsky_service(
|
|||||||
wait_with_heartbeat(8, label="CDN propagation")
|
wait_with_heartbeat(8, label="CDN propagation")
|
||||||
blob_ref = models.BlobRef.from_dict(blob_dict)
|
blob_ref = models.BlobRef.from_dict(blob_dict)
|
||||||
logging.info("✅ Video processed successfully.")
|
logging.info("✅ Video processed successfully.")
|
||||||
return models.AppBskyEmbedVideo.Main(video=blob_ref, alt=alt_text)
|
return models.AppBskyEmbedVideo.Main(video=blob_ref, alt="")
|
||||||
|
|
||||||
if state == "JOB_STATE_FAILED":
|
if state == "JOB_STATE_FAILED":
|
||||||
logging.error(f"❌ Video processing failed: {job_status}")
|
logging.error(f"❌ Video processing failed: {job_status}")
|
||||||
@@ -428,6 +365,97 @@ def upload_video_via_bsky_service(
|
|||||||
logging.error("❌ Video processing timed out.")
|
logging.error("❌ Video processing timed out.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def upload_video_via_bsky_service(
|
||||||
|
client: Client,
|
||||||
|
video_path: str,
|
||||||
|
service_url: str,
|
||||||
|
alt_text: str = "",
|
||||||
|
) -> models.AppBskyEmbedVideo.Main | None:
|
||||||
|
"""
|
||||||
|
Upload via centralized video.bsky.app service.
|
||||||
|
|
||||||
|
Critical compatibility fixes:
|
||||||
|
- aud must be user's PDS DID (e.g. did:web:eurosky.social)
|
||||||
|
- lxm must be com.atproto.repo.uploadBlob
|
||||||
|
- handle 409 already_exists by reusing returned jobId
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not os.path.exists(video_path):
|
||||||
|
logging.error(f"❌ Video file not found: {video_path}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
with open(video_path, "rb") as f:
|
||||||
|
video_bytes = f.read()
|
||||||
|
|
||||||
|
size_mb = len(video_bytes) / (1024 * 1024)
|
||||||
|
logging.info(f"🎬 [video.bsky.app] Uploading: {video_path} ({size_mb:.2f} MB)")
|
||||||
|
|
||||||
|
VIDEO_HOST = "https://video.bsky.app"
|
||||||
|
pds_did = pds_did_from_service_url(service_url)
|
||||||
|
|
||||||
|
try:
|
||||||
|
params = models.ComAtprotoServerGetServiceAuth.Params(
|
||||||
|
aud=pds_did,
|
||||||
|
lxm="com.atproto.repo.uploadBlob",
|
||||||
|
exp=int(time.time()) + 60 * 30,
|
||||||
|
)
|
||||||
|
upload_auth = client.com.atproto.server.get_service_auth(params)
|
||||||
|
except Exception:
|
||||||
|
upload_auth = client.com.atproto.server.get_service_auth(
|
||||||
|
{
|
||||||
|
"aud": pds_did,
|
||||||
|
"lxm": "com.atproto.repo.uploadBlob",
|
||||||
|
"exp": int(time.time()) + 60 * 30,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
token = _extract_service_auth_token(upload_auth)
|
||||||
|
if not token:
|
||||||
|
logging.error("❌ Failed to extract service auth token.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
user_did = client.me.did
|
||||||
|
upload_name = random_video_name(".mp4")
|
||||||
|
logging.info(f"🎞️ Upload name: {upload_name}")
|
||||||
|
|
||||||
|
upload_url = (
|
||||||
|
f"{VIDEO_HOST}/xrpc/app.bsky.video.uploadVideo"
|
||||||
|
f"?did={user_did}&name={upload_name}"
|
||||||
|
)
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
"Content-Type": "video/mp4",
|
||||||
|
}
|
||||||
|
|
||||||
|
upload_resp = requests.post(upload_url, headers=headers, data=video_bytes, timeout=180)
|
||||||
|
|
||||||
|
if upload_resp.status_code not in (200, 409):
|
||||||
|
logging.error(f"❌ video.bsky.app upload failed: {upload_resp.status_code} - {upload_resp.text}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
body = upload_resp.json()
|
||||||
|
|
||||||
|
if upload_resp.status_code == 409:
|
||||||
|
if body.get("error") == "already_exists" and body.get("jobId"):
|
||||||
|
logging.info("ℹ️ Video already processed on video.bsky.app. Reusing existing job.")
|
||||||
|
else:
|
||||||
|
logging.error(f"❌ video.bsky.app returned 409 without reusable jobId: {body}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
job_id = body.get("jobId")
|
||||||
|
if not job_id:
|
||||||
|
logging.error(f"❌ No jobId returned from video service. Response: {body}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
logging.info(f"⏳ Job {job_id} accepted — polling status...")
|
||||||
|
embed = _poll_video_job(VIDEO_HOST, job_id)
|
||||||
|
if not embed:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# inject alt text after job result
|
||||||
|
return models.AppBskyEmbedVideo.Main(video=embed.video, alt=alt_text)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"❌ video.bsky.app upload failed: {repr(e)}")
|
logging.error(f"❌ video.bsky.app upload failed: {repr(e)}")
|
||||||
return None
|
return None
|
||||||
@@ -513,7 +541,7 @@ def post_to_bsky(
|
|||||||
|
|
||||||
record = {
|
record = {
|
||||||
"$type": "app.bsky.feed.post",
|
"$type": "app.bsky.feed.post",
|
||||||
"text": post_text, # guaranteed plain string
|
"text": post_text,
|
||||||
"createdAt": time.strftime("%Y-%m-%dT%H:%M:%S.000Z", time.gmtime()),
|
"createdAt": time.strftime("%Y-%m-%dT%H:%M:%S.000Z", time.gmtime()),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -525,7 +553,6 @@ def post_to_bsky(
|
|||||||
|
|
||||||
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:
|
try:
|
||||||
resp = client.com.atproto.repo.create_record(
|
resp = client.com.atproto.repo.create_record(
|
||||||
models.ComAtprotoRepoCreateRecord.Data(
|
models.ComAtprotoRepoCreateRecord.Data(
|
||||||
|
|||||||
Reference in New Issue
Block a user