···
class BlueskyPost(cross.Post):
102
-
def __init__(self, pds_url: str, did: str, post: dict) -> None:
102
+
def __init__(self, post: dict, attachments: list[media_util.MediaInfo]) -> None:
self.tokens = tokenize_post(post)
···
self.cw = ', '.join([str(label['val']).replace('-', ' ') for label in labels])
118
-
def get_blob_url(blob: str):
119
-
nonlocal pds_url, did
120
-
return f'{pds_url}/xrpc/com.atproto.sync.getBlob?did={did}&cid={blob}'
122
-
attachments: list[cross.MediaAttachment] = []
123
-
embed = self.post.get('embed', {})
124
-
if embed.get('$type') == 'app.bsky.embed.images':
125
-
model = get_model_or_create(embed, model=models.AppBskyEmbedImages.Main)
126
-
assert isinstance(model, models.AppBskyEmbedImages.Main)
128
-
for image in model.images:
129
-
attachments.append(BlueskyAttachment(
130
-
get_blob_url(image.image.cid.encode()),
133
-
elif embed.get('$type') == 'app.bsky.embed.video':
134
-
model = get_model_or_create(embed, model=models.AppBskyEmbedVideo.Main)
135
-
assert isinstance(model, models.AppBskyEmbedVideo.Main)
137
-
attachments.append(BlueskyAttachment(
138
-
get_blob_url(model.video.cid.encode()),
139
-
'video', model.alt if model.alt else ''
self.attachments = attachments
def get_tokens(self) -> list[cross.Token]:
···
def is_sensitive(self) -> bool:
return self.post.get('labels', {}).get('values') or False
164
-
def get_attachments(self) -> list[cross.MediaAttachment]:
165
-
return self.attachments or []
167
-
class BlueskyAttachment(cross.MediaAttachment):
168
-
def __init__(self, url: str, type: str, alt: str) -> None:
174
-
def get_url(self) -> str:
177
-
def get_type(self) -> str | None:
180
-
def create_meta(self, bytes: bytes) -> cross.MediaMeta:
181
-
o_meta = media_util.get_media_meta(bytes)
182
-
return cross.MediaMeta(o_meta['width'], o_meta['height'], o_meta.get('duration', -1))
184
-
def get_alt(self) -> str:
140
+
def get_attachments(self) -> list[media_util.MediaInfo]:
141
+
return self.attachments
class BlueskyInput(cross.Input):
def __init__(self, settings: dict, db: DataBaseWorker) -> None:
···
LOGGER.info("Crossposting '%s'...", post_ref)
213
-
cross_post = BlueskyPost(self.pds, self.user_id, post)
170
+
def get_blob_url(blob: str):
171
+
return f'{self.pds}/xrpc/com.atproto.sync.getBlob?did={self.user_id}&cid={blob}'
173
+
attachments: list[media_util.MediaInfo] = []
174
+
embed = post.get('embed', {})
175
+
if embed.get('$type') == 'app.bsky.embed.images':
176
+
model = get_model_or_create(embed, model=models.AppBskyEmbedImages.Main)
177
+
assert isinstance(model, models.AppBskyEmbedImages.Main)
179
+
for image in model.images:
180
+
url = get_blob_url(image.image.cid.encode())
181
+
LOGGER.info("Downloading %s...", url)
182
+
io = media_util.download_media(url, image.alt)
184
+
LOGGER.error("Skipping '%s'. Failed to download media!", post_ref)
186
+
attachments.append(io)
187
+
elif embed.get('$type') == 'app.bsky.embed.video':
188
+
model = get_model_or_create(embed, model=models.AppBskyEmbedVideo.Main)
189
+
assert isinstance(model, models.AppBskyEmbedVideo.Main)
190
+
url = get_blob_url(model.video.cid.encode())
191
+
LOGGER.info("Downloading %s...", url)
192
+
io = media_util.download_media(url, model.alt if model.alt else '')
194
+
LOGGER.error("Skipping '%s'. Failed to download media!", post_ref)
196
+
attachments.append(io)
198
+
cross_post = BlueskyPost(post, attachments)
output.accept_post(cross_post)
···
raise Exception("Account app password not provided!")
did, pds = resolve_identity(
320
-
handle=util.as_envvar(settings.get('hanlde')),
305
+
handle=util.as_envvar(settings.get('handle')),
did=util.as_envvar(settings.get('did')),
pds=util.as_envvar(settings.get('pds'))
···
361
-
def _split_attachments(self, attachments: list[cross.MediaAttachment]):
362
-
sup_media: list[cross.MediaAttachment] = []
363
-
unsup_media: list[cross.MediaAttachment] = []
346
+
def _split_attachments(self, attachments: list[media_util.MediaInfo]):
347
+
sup_media: list[media_util.MediaInfo] = []
348
+
unsup_media: list[media_util.MediaInfo] = []
365
-
for attachment in attachments:
366
-
attachment_type = attachment.get_type()
367
-
if not attachment_type:
370
-
if attachment_type in {'video', 'image'}: # TODO convert gifs to videos
371
-
sup_media.append(attachment)
350
+
for a in attachments:
351
+
if a.mime.startswith('image/') or a.mime.startswith('video/'): # TODO convert gifs to videos
352
+
sup_media.append(a)
373
-
unsup_media.append(attachment)
354
+
unsup_media.append(a)
return (sup_media, unsup_media)
def _split_media_per_post(
tokens: list[client_utils.TextBuilder],
380
-
media: list[cross.MediaAttachment]):
361
+
media: list[media_util.MediaInfo]):
posts: list[dict] = [{"tokens": tokens, "attachments": []} for tokens in tokens]
available_indices: list[int] = list(range(len(posts)))
···
402
-
if att.get_type() == 'video':
383
+
if att.mime.startswith('video/'):
current_image_post_idx = None
idx = pop_next_empty_index()
posts[idx]["attachments"].append(att)
406
-
elif att.get_type() == 'image':
387
+
elif att.mime.startswith('image/'):
current_image_post_idx is not None
and len(posts[current_image_post_idx]["attachments"]) < 4
···
posts[idx]["attachments"].append(att)
current_image_post_idx = idx
417
-
result: list[tuple[client_utils.TextBuilder, list[cross.MediaAttachment]]] = []
398
+
result: list[tuple[client_utils.TextBuilder, list[media_util.MediaInfo]]] = []
result.append((p["tokens"], p["attachments"]))
···
tokens.append(cross.TextToken('\n'))
for i, attachment in enumerate(unsup_media):
tokens.append(cross.LinkToken(
467
-
attachment.get_url(),
468
-
f"[{media_util.get_filename_from_url(attachment.get_url())}]"
449
+
f"[{media_util.get_filename_from_url(attachment.url)}]"
tokens.append(cross.TextToken(' '))
473
-
split_tokens: list[list[cross.Token]] = util.split_tokens(tokens, 300)
454
+
split_tokens: list[list[cross.Token]] = cross.split_tokens(tokens, 300)
post_text: list[client_utils.TextBuilder] = []
# convert tokens into rich text. skip post if contains unsupported tokens
···
post_text = [client_utils.TextBuilder().text('')]
488
-
# download media first. increased RAM usage, but more reliable
491
-
if m.get_type() == 'image':
492
-
image_bytes = media_util.download_blob(m.get_url(), max_bytes=2_000_000)
493
-
if not image_bytes:
494
-
LOGGER.error("Skipping post_id '%s', failed to download attachment! File too large?", post.get_id())
496
-
m.bytes = image_bytes
497
-
elif m.get_type() == 'video':
498
-
video_bytes = media_util.download_blob(m.get_url(), max_bytes=100_000_000)
499
-
if not video_bytes:
500
-
LOGGER.error("Skipping post_id '%s', failed to download attachment! File too large?", post.get_id())
502
-
m.bytes = video_bytes
470
+
if m.mime.startswith('image/'):
471
+
if len(m.io) > 2_000_000:
472
+
LOGGER.error("Skipping post_id '%s', failed to download attachment! File too large.", post.get_id())
475
+
if m.mime.startswith('video/'):
476
+
if len(m.io) > 100_000_000:
477
+
LOGGER.error("Skipping post_id '%s', failed to download attachment! File too large?", post.get_id())
created_records: list[models.AppBskyFeedPost.CreateRecordResponse] = []
baked_media = self._split_media_per_post(post_text, sup_media)
···
created_records.append(new_post)
# if a single post is an image - everything else is an image
523
-
if attachments[0].get_type() == 'image':
499
+
if attachments[0].mime.startswith('image/'):
image_alts: list[str] = []
image_aspect_ratios: list[models.AppBskyEmbedDefs.AspectRatio] = []
for attachment in attachments:
529
-
assert attachment.bytes
530
-
image_io = media_util.compress_image(attachment.bytes, quality=100)
531
-
metadata = attachment.create_meta(image_io)
505
+
image_io = media_util.compress_image(attachment.io, quality=100)
506
+
metadata = media_util.get_media_meta(image_io)
if len(image_io) > 1_000_000:
534
-
LOGGER.info("Compressing %s...", attachment.get_url())
509
+
LOGGER.info("Compressing %s...", attachment.name)
510
+
image_io = media_util.compress_image(image_io)
537
-
image_alts.append(attachment.get_alt())
513
+
image_alts.append(attachment.alt)
image_aspect_ratios.append(models.AppBskyEmbedDefs.AspectRatio(
539
-
width=metadata.get_width(),
540
-
height=metadata.get_height()
515
+
width=metadata['width'],
516
+
height=metadata['height']
new_post = self.bsky.send_images(
···
reply_ref = models.create_strong_ref(new_post)
created_records.append(new_post)
else: # video is guarantedd to be one
562
-
video_data = attachments[0]
563
-
assert video_data.bytes
564
-
video_io = video_data.bytes
566
-
metadata = video_data.create_meta(video_io)
567
-
if metadata.get_duration() > 180:
538
+
metadata = media_util.get_media_meta(attachments[0].io)
539
+
if metadata['duration'] > 180:
LOGGER.info("Skipping post_id '%s', video attachment too long!", post.get_id())
571
-
probe = media_util.probe_bytes(video_io)
572
-
format_name = probe['format']['format_name']
573
-
if 'mp4' not in format_name.split(','):
574
-
LOGGER.error("Converting %s to mp4...", video_data.get_url())
543
+
video_io = attachments[0].io
544
+
if attachments[0].mime != 'video/mp4':
545
+
LOGGER.error("Converting %s to mp4...", attachments[0].name)
video_io = media_util.convert_to_mp4(video_io)
aspect_ratio = models.AppBskyEmbedDefs.AspectRatio(
578
-
width=metadata.get_width(),
579
-
height=metadata.get_height()
549
+
width=metadata['width'],
550
+
height=metadata['height']
new_post = self.bsky.send_video(
video_aspect_ratio=aspect_ratio,
586
-
video_alt=video_data.get_alt(),
557
+
video_alt=attachments[0].alt,
reply_to= models.AppBskyFeedPost.ReplyRef(