Monorepo for Aesthetic.Computer
aesthetic.computer
1#!/usr/bin/env node
2
3/**
4 * Bulk Share Paintings to Bluesky
5 *
6 * Post multiple paintings from the aesthetic.computer TV feed to Bluesky.
7 *
8 * Usage:
9 * node bulk-share-paintings.mjs 10
10 * node bulk-share-paintings.mjs 10 --delay 3000
11 */
12
13import { AtpAgent, RichText } from '@atproto/api'
14import { config } from 'dotenv'
15
16config()
17
18const BSKY_SERVICE = process.env.BSKY_SERVICE || 'https://bsky.social'
19const BSKY_IDENTIFIER = process.env.BSKY_IDENTIFIER
20const BSKY_APP_PASSWORD = process.env.BSKY_APP_PASSWORD
21const AC_API = 'https://aesthetic.computer'
22
23const args = process.argv.slice(2)
24const count = parseInt(args[0]) || 10
25const delayIndex = args.indexOf('--delay')
26const delay = delayIndex !== -1 ? parseInt(args[delayIndex + 1]) : 2000
27
28let agent = null
29
30async function login() {
31 if (!BSKY_IDENTIFIER || !BSKY_APP_PASSWORD) {
32 console.error('❌ Error: Missing Bluesky credentials')
33 process.exit(1)
34 }
35
36 console.log(`🔐 Logging in as @${BSKY_IDENTIFIER}...`)
37 agent = new AtpAgent({ service: BSKY_SERVICE })
38 await agent.login({
39 identifier: BSKY_IDENTIFIER,
40 password: BSKY_APP_PASSWORD
41 })
42 console.log('✅ Logged in successfully\n')
43}
44
45async function fetchPaintings(limit) {
46 console.log(`📡 Fetching ${limit} paintings from TV feed...\n`)
47
48 const response = await fetch(`${AC_API}/api/tv?limit=${limit}`)
49 if (!response.ok) {
50 throw new Error(`HTTP ${response.status}: ${response.statusText}`)
51 }
52
53 const data = await response.json()
54
55 if (!data.media || !data.media.paintings) {
56 throw new Error('No paintings found')
57 }
58
59 // Filter out paintings we might have already posted (basic check)
60 const paintings = data.media.paintings.filter(p => p.owner.handle)
61
62 console.log(`✅ Found ${paintings.length} paintings with handles\n`)
63 return paintings
64}
65
66async function downloadPainting(url) {
67 const response = await fetch(url)
68 if (!response.ok) {
69 throw new Error(`HTTP ${response.status}`)
70 }
71 const arrayBuffer = await response.arrayBuffer()
72 return Buffer.from(arrayBuffer)
73}
74
75async function postPainting(painting, index, total) {
76 const handle = painting.owner.handle.replace('@', '')
77 const slug = painting.slug
78 const acUrl = `https://aesthetic.computer/painting~@${handle}/${slug}`
79
80 console.log(`\n[${index + 1}/${total}] 🎨 Posting painting by @${handle}`)
81 console.log(` Slug: ${slug}`)
82 console.log(` URL: ${acUrl}`)
83
84 try {
85 // Download painting
86 const imageBuffer = await downloadPainting(painting.media.url)
87 console.log(` ✅ Downloaded ${imageBuffer.length} bytes`)
88
89 // Upload to Bluesky
90 const uploadResponse = await agent.uploadBlob(imageBuffer, {
91 encoding: 'image/png'
92 })
93 console.log(` ✅ Uploaded to Bluesky`)
94
95 // Create post with RichText for proper link formatting
96 const postText = `New painting by @${handle} 🎨✨\n\n${acUrl}`
97
98 const rt = new RichText({ text: postText })
99 await rt.detectFacets(agent)
100
101 const postRecord = {
102 text: rt.text,
103 facets: rt.facets,
104 createdAt: new Date().toISOString(),
105 embed: {
106 $type: 'app.bsky.embed.images',
107 images: [{
108 image: uploadResponse.data.blob,
109 alt: `Painting by @${handle} on aesthetic.computer`
110 }]
111 }
112 }
113
114 const response = await agent.post(postRecord)
115 const postId = response.uri.split('/').pop()
116 const bskyUrl = `https://bsky.app/profile/${BSKY_IDENTIFIER}/post/${postId}`
117
118 console.log(` ✅ Posted! ${bskyUrl}`)
119
120 return { success: true, handle, slug, bskyUrl }
121 } catch (error) {
122 console.error(` ❌ Failed: ${error.message}`)
123 return { success: false, handle, slug, error: error.message }
124 }
125}
126
127async function sleep(ms) {
128 return new Promise(resolve => setTimeout(resolve, ms))
129}
130
131async function main() {
132 console.log(`\n🚀 Bulk Share Paintings to Bluesky\n`)
133 console.log(` Count: ${count}`)
134 console.log(` Delay: ${delay}ms between posts\n`)
135 console.log('═'.repeat(50) + '\n')
136
137 try {
138 // Login once
139 await login()
140
141 // Fetch paintings
142 const paintings = await fetchPaintings(count * 2) // Fetch extra in case some fail
143
144 if (paintings.length === 0) {
145 console.error('❌ No paintings to post')
146 process.exit(1)
147 }
148
149 // Post paintings
150 const results = []
151 const toPost = paintings.slice(0, count)
152
153 for (let i = 0; i < toPost.length; i++) {
154 const result = await postPainting(toPost[i], i, toPost.length)
155 results.push(result)
156
157 // Delay between posts (except for the last one)
158 if (i < toPost.length - 1) {
159 console.log(` ⏳ Waiting ${delay}ms...`)
160 await sleep(delay)
161 }
162 }
163
164 // Summary
165 console.log('\n' + '═'.repeat(50))
166 console.log('✨ Summary\n')
167
168 const successful = results.filter(r => r.success).length
169 const failed = results.filter(r => !r.success).length
170
171 console.log(`✅ Successful: ${successful}`)
172 console.log(`❌ Failed: ${failed}`)
173 console.log(`📊 Total: ${results.length}\n`)
174
175 if (successful > 0) {
176 console.log('🔗 Posted paintings:')
177 results.filter(r => r.success).forEach((r, i) => {
178 console.log(` ${i + 1}. @${r.handle} - ${r.bskyUrl}`)
179 })
180 }
181
182 if (failed > 0) {
183 console.log('\n⚠️ Failed paintings:')
184 results.filter(r => !r.success).forEach((r, i) => {
185 console.log(` ${i + 1}. @${r.handle}/${r.slug} - ${r.error}`)
186 })
187 }
188
189 console.log()
190 } catch (error) {
191 console.error('\n💥 Error:', error.message)
192 process.exit(1)
193 }
194}
195
196main()