Import Instagram archive to a Bluesky account
9
fork

Configure Feed

Select the types of activity you want to include in your feed.

Simplify Bluesky video upload and embed handling

- Refactor video upload process to use direct blob upload
- Remove complex job status polling mechanism
- Update VideoEmbed and VideoEmbedPost interfaces
- Streamline video and image embed generation logic
- Improve error handling for media uploads

+29 -51
+29 -51
src/bluesky.ts
··· 6 6 alt: string; 7 7 buffer: Buffer; 8 8 mimeType: string; 9 - aspectRatio?: { width: number; height: number }; 9 + size?: number; 10 10 video?: { 11 11 ref: BlobRef; 12 12 mimeType: string; ··· 14 14 }; 15 15 } 16 16 17 + export interface VideoEmbedPost { 18 + $type: 'app.bsky.embed.video'; 19 + video: BlobRef; 20 + mimeType: string; 21 + size: number; 22 + } 23 + 17 24 export interface ImageEmbed { 18 25 $type: 'app.bsky.embed.images#image'; 19 26 alt: string; 20 27 image: Buffer | BlobRef; 21 - mimeType: string; 22 28 } 23 29 24 30 export interface ImagesEmbed { ··· 27 33 } 28 34 29 35 type EmbeddedMedia = VideoEmbed | ImageEmbed[] | ImagesEmbed; 36 + type PostEmbed = VideoEmbedPost | ImagesEmbed; 30 37 31 38 export class BlueskyClient { 32 39 private readonly agent: AtpAgent; ··· 55 62 /** 56 63 * Upload video file and get blob reference 57 64 */ 58 - async uploadVideo(buffer: Buffer, mimeType: 'video/mp4' = 'video/mp4'): Promise<BlobRef> { 65 + async uploadVideo(buffer: Buffer, mimeType: string = 'video/mp4'): Promise<BlobRef> { 59 66 try { 60 67 logger.debug('Starting video upload process...'); 61 - 62 - // Step 1: Upload video and get job ID 63 - const uploadResponse = await this.agent.api.app.bsky.video.uploadVideo(buffer, { 64 - encoding: mimeType, 65 - headers: { 66 - 'Content-Type': mimeType 67 - } 68 - }); 69 - 70 - if (!uploadResponse?.data?.jobStatus?.jobId) { 71 - throw new Error('Failed to get job ID from video upload'); 72 - } 73 - 74 - const jobId = uploadResponse.data.jobStatus.jobId; 75 - logger.debug(`Video upload started with job ID: ${jobId}`); 76 - 77 - // Step 2: Poll job status until complete 78 - const maxAttempts = 30; // 5 minutes max (10 second intervals) 79 - let attempts = 0; 68 + const response = await this.agent.uploadBlob(buffer, { encoding: mimeType }); 80 69 81 - while (attempts < maxAttempts) { 82 - const statusResponse = await this.agent.api.app.bsky.video.getJobStatus({ 83 - jobId: jobId 84 - }); 85 - 86 - const status = statusResponse.data.jobStatus; 87 - 88 - if (status.state === 'JOB_STATE_COMPLETED' && status.blob) { 89 - logger.debug(`Video upload completed with blob: ${status.blob}`); 90 - return status.blob; 91 - } 92 - 93 - if (status.state === 'JOB_STATE_FAILED') { 94 - throw new Error(`Video upload failed: ${status.error || 'Unknown error'}`); 95 - } 96 - 97 - // Wait 10 seconds before checking again 98 - await new Promise(resolve => setTimeout(resolve, 10000)); 99 - attempts++; 70 + if (!response?.data?.blob) { 71 + throw new Error('Failed to get video upload reference'); 100 72 } 101 73 102 - throw new Error('Video upload timed out'); 74 + return response.data.blob; 103 75 } catch (error) { 104 76 logger.error('Failed to upload video:', error); 105 77 throw error; ··· 125 97 } 126 98 } 127 99 128 - private determineEmbed(embeddedMedia: EmbeddedMedia) { 100 + private determineEmbed(embeddedMedia: EmbeddedMedia): PostEmbed | undefined { 129 101 if (!embeddedMedia) return undefined; 130 102 131 103 // Handle video embed 132 104 if (!Array.isArray(embeddedMedia) && embeddedMedia.$type === 'app.bsky.embed.video') { 133 105 return { 134 106 $type: 'app.bsky.embed.video', 135 - video: { 136 - $type: 'blob', 137 - ref: embeddedMedia.video!.ref, 138 - mimeType: embeddedMedia.mimeType, 139 - size: embeddedMedia.video!.size 140 - }, 141 - aspectRatio: embeddedMedia.aspectRatio 107 + video: embeddedMedia.video!.ref, 108 + mimeType: embeddedMedia.mimeType, 109 + size: embeddedMedia.video!.size 142 110 }; 143 111 } 144 112 ··· 149 117 images: embeddedMedia.map(img => ({ 150 118 $type: 'app.bsky.embed.images#image', 151 119 alt: img.alt, 152 - image: img.image as BlobRef // At this point it should be a BlobRef 120 + image: img.image as BlobRef 153 121 })) 154 122 }; 155 123 } ··· 176 144 $type: 'app.bsky.embed.images', 177 145 images: uploadedImages 178 146 }; 147 + } else if (embeddedMedia?.$type === 'app.bsky.embed.video') { 148 + // Upload video first 149 + const blob = await this.uploadVideo(embeddedMedia.buffer, embeddedMedia.mimeType); 150 + embeddedMedia.video = { 151 + ref: blob, 152 + mimeType: embeddedMedia.mimeType, 153 + size: embeddedMedia.buffer.length 154 + }; 155 + // Now transform the embed 156 + embeddedMedia = this.determineEmbed(embeddedMedia); 179 157 } 180 158 181 159 const rt = new RichText({ text: postText });