Monorepo for Aesthetic.Computer aesthetic.computer
at main 176 lines 6.2 kB view raw
1#!/usr/bin/env node 2// Retry a failed tape bake 3// Usage: node retry-tape-bake.mjs <code> 4 5import { connect } from '../system/backend/database.mjs'; 6import { S3Client, PutObjectAclCommand, HeadObjectCommand } from '@aws-sdk/client-s3'; 7import fetch from 'node-fetch'; 8 9async function retryTapeBake(code) { 10 console.log(`\n🔄 Retrying tape bake for code: ${code}\n`); 11 12 const ART_KEY = process.env.ART_KEY || process.env.DO_SPACES_KEY; 13 const ART_SECRET = process.env.ART_SECRET || process.env.DO_SPACES_SECRET; 14 const OVEN_URL = process.env.OVEN_URL || 'https://oven.aesthetic.computer'; 15 const OVEN_CALLBACK_SECRET = process.env.OVEN_CALLBACK_SECRET; 16 17 const database = await connect(); 18 const tapes = database.db.collection('tapes'); 19 20 try { 21 // Find the tape 22 const tape = await tapes.findOne({ code }); 23 24 if (!tape) { 25 console.error(`❌ Tape not found with code: ${code}`); 26 process.exit(1); 27 } 28 29 console.log(`📼 Found tape: ${tape.slug}`); 30 console.log(` MongoDB ID: ${tape._id}`); 31 console.log(` Bucket: ${tape.bucket}`); 32 33 // Get user info 34 let user = null; 35 if (tape.user) { 36 const users = database.db.collection('users'); 37 user = await users.findOne({ _id: tape.user }); 38 console.log(` User: ${user?.email || tape.user}`); 39 } else { 40 console.log(` User: anonymous`); 41 } 42 43 // Construct ZIP key and URL 44 // Use user._id as the sub (it's the Auth0 sub) 45 const key = user ? `${user._id}/video/${tape.slug}.zip` : `${tape.slug}.zip`; 46 const zipUrl = `https://${tape.bucket}.sfo3.digitaloceanspaces.com/${key}`; 47 48 console.log(`\n📦 ZIP Info:`); 49 console.log(` Key: ${key}`); 50 console.log(` URL: ${zipUrl}`); 51 52 // Step 1: Verify the media URL works 53 console.log(`\n� ZIP Info:`); 54 console.log(` Key: ${key}`); 55 console.log(` URL: ${zipUrl}`); 56 57 // Step 1: Check if ZIP exists and is accessible 58 console.log(`\n🔍 Step 1: Checking ZIP accessibility...`); 59 try { 60 const headResponse = await fetch(zipUrl, { method: 'HEAD' }); 61 62 if (headResponse.ok) { 63 console.log(` ✅ ZIP is already publicly accessible`); 64 } else { 65 console.log(` ❌ ZIP not accessible (${headResponse.status}), will try to fix ACL`); 66 67 // Step 2: Fix the ACL 68 console.log(`\n🔧 Step 2: Setting public-read ACL...`); 69 70 const s3Client = new S3Client({ 71 endpoint: `https://sfo3.digitaloceanspaces.com`, 72 region: 'us-east-1', // Required for DigitalOcean Spaces 73 credentials: { 74 accessKeyId: ART_KEY, 75 secretAccessKey: ART_SECRET, 76 }, 77 }); 78 79 const aclCommand = new PutObjectAclCommand({ 80 Bucket: tape.bucket, 81 Key: key, 82 ACL: 'public-read', 83 }); 84 85 await s3Client.send(aclCommand); 86 console.log(` ✅ ACL set to public-read`); 87 88 // Wait for ACL propagation 89 console.log(` ⏳ Waiting 500ms for ACL propagation...`); 90 await new Promise(resolve => setTimeout(resolve, 500)); 91 92 // Verify it worked 93 const verifyResponse = await fetch(zipUrl, { method: 'HEAD' }); 94 if (verifyResponse.ok) { 95 console.log(` ✅ ZIP is now publicly accessible`); 96 } else { 97 console.log(` ⚠️ ZIP still not accessible (${verifyResponse.status}), but continuing...`); 98 } 99 } 100 } catch (error) { 101 console.error(` ❌ Error checking/fixing ZIP: ${error.message}`); 102 console.log(` Continuing anyway...`); 103 } 104 105 // Step 3: Send to oven 106 console.log(`\n🔥 Step 3: Sending to oven for processing...`); 107 108 const isDev = process.env.CONTEXT === 'dev' || process.env.NODE_ENV === 'development'; 109 const baseUrl = isDev ? 'https://localhost:8888' : (process.env.URL || 'https://aesthetic.computer'); 110 const callbackUrl = `${baseUrl}/api/oven-complete`; 111 112 const payload = { 113 mongoId: tape._id.toString(), 114 slug: tape.slug, 115 code: tape.code, 116 zipUrl, 117 callbackUrl, 118 callbackSecret: OVEN_CALLBACK_SECRET, 119 }; 120 121 console.log(` Oven: ${OVEN_URL}/bake`); 122 console.log(` Callback: ${callbackUrl}`); 123 console.log(` Secret: ${OVEN_CALLBACK_SECRET ? OVEN_CALLBACK_SECRET.substring(0, 10) + '...' : 'MISSING!'}`); 124 125 const ovenResponse = await fetch(`${OVEN_URL}/bake`, { 126 method: 'POST', 127 headers: { 'Content-Type': 'application/json' }, 128 body: JSON.stringify(payload), 129 }); 130 131 if (!ovenResponse.ok) { 132 const errorText = await ovenResponse.text(); 133 console.error(` ❌ Oven request failed: ${ovenResponse.status}`); 134 console.error(` Response: ${errorText}`); 135 process.exit(1); 136 } 137 138 const ovenResult = await ovenResponse.json(); 139 console.log(` ✅ Oven accepted the bake request`); 140 console.log(` Response:`, ovenResult); 141 142 console.log(`\n✨ Success! Tape ${code} has been queued for processing.`); 143 console.log(` Check the oven dashboard at: ${OVEN_URL}`); 144 console.log(` The MP4 will be available at: https://aesthetic.computer/!${code}\n`); 145 146 } catch (error) { 147 console.error(`\n❌ Fatal error:`, error.message); 148 console.error(error); 149 process.exit(1); 150 } finally { 151 await database.disconnect(); 152 } 153} 154 155// Parse command line 156const code = process.argv[2]; 157 158if (!code) { 159 console.error('Usage: node retry-tape-bake.mjs <code>'); 160 console.error('Example: node retry-tape-bake.mjs 5b9'); 161 process.exit(1); 162} 163 164const ART_KEY = process.env.ART_KEY || process.env.DO_SPACES_KEY || process.env.SPACES_KEY; 165const ART_SECRET = process.env.ART_SECRET || process.env.DO_SPACES_SECRET || process.env.SPACES_SECRET; 166const OVEN_CALLBACK_SECRET = process.env.OVEN_CALLBACK_SECRET; 167 168if (!ART_KEY || !ART_SECRET) { 169 console.error('❌ Missing required environment variables: ART_KEY/DO_SPACES_KEY/SPACES_KEY and ART_SECRET/DO_SPACES_SECRET/SPACES_SECRET'); 170 process.exit(1); 171} 172 173retryTapeBake(code).catch(err => { 174 console.error('💥 Fatal error:', err); 175 process.exit(1); 176});