Monorepo for Aesthetic.Computer
aesthetic.computer
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});