← back to Animate Museum Posts
src/index.js
232 lines
import cron from 'node-cron';
import { fetchFromMuseums } from './fetchFromMuseums.js';
import { fetchRandomArtwork } from './fetchArtwork.js';
import { postToX, testCredentials } from './postToX.js';
// Animation disabled - posting photos only
// import { animateArtwork, getAnimationStats } from './animateWithGrok.js';
import { startHealthServer, updateLastPost, setRunning } from './healthServer.js';
import { validateCredentials } from './validateCredentials.js';
import { isRecentlyPosted, markAsPosted } from './duplicateTracker.js';
import { notifySuccess, notifyError } from './slackNotify.js';
import { withRetry } from './utils/retry.js';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import dotenv from 'dotenv';
dotenv.config();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
async function processAndPost() {
console.log('\n========================================');
console.log('Starting museum art post process...');
console.log('Time:', new Date().toISOString());
console.log('========================================\n');
try {
// Step 1: Fetch artwork from museums with retry
console.log('Step 1: Fetching artwork from museums...');
let artwork;
let attempts = 0;
const maxAttempts = 5;
while (attempts < maxAttempts) {
attempts++;
try {
artwork = await withRetry(
() => fetchFromMuseums(),
{ maxRetries: 2, initialDelay: 2000 }
);
// Check for duplicates
if (isRecentlyPosted(artwork.objectNumber)) {
console.log(`Skipping duplicate: ${artwork.title} (posted recently)`);
continue;
}
break;
} catch (error) {
console.log(`Fetch attempt ${attempts} failed:`, error.message);
if (attempts === maxAttempts) {
// Final fallback to hardcoded
console.log('Using hardcoded fallback...');
artwork = await fetchRandomArtwork();
}
}
}
console.log(`Fetched: "${artwork.title}" by ${artwork.artist}`);
console.log(`Source: ${artwork.museum || artwork.sourceMuseum || 'Hardcoded'}`);
// Step 2: Prepare photo for posting (no animation)
console.log('\nStep 2: Preparing photo for posting...');
const animationResult = {
originalImage: artwork.localPath,
isAnimated: false,
animationType: 'static_photo',
artworkMetadata: artwork
};
console.log(`Using static photo: ${artwork.localPath}`);
// Step 3: Post to X.com with retry
console.log('\nStep 3: Posting to X.com...');
const tweet = await withRetry(
() => postToX(animationResult),
{ maxRetries: 3, initialDelay: 5000 }
);
console.log('\nSuccessfully posted!');
console.log(`Tweet ID: ${tweet.data.id}`);
// Mark as posted to prevent duplicates
markAsPosted(artwork, tweet.data.id);
// Send Slack notification
await notifySuccess(artwork, tweet.data.id);
// Cleanup old files
cleanupOldFiles();
// Photo posted successfully
console.log('\nPhoto post completed!');
return { success: true, artwork, tweet };
} catch (error) {
console.error('\nError in process:', error);
// Log error
const errorLog = {
timestamp: new Date().toISOString(),
error: error.message,
stack: error.stack
};
const logsDir = path.join(__dirname, '..', 'logs');
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir, { recursive: true });
}
const errorFile = path.join(logsDir, 'errors.log');
fs.appendFileSync(errorFile, JSON.stringify(errorLog) + '\n');
// Send Slack error notification
await notifyError(error.message, 'processAndPost');
return { success: false, error: error.message };
}
}
function cleanupOldFiles() {
try {
const outputDir = path.join(__dirname, '..', 'output');
if (!fs.existsSync(outputDir)) return;
const files = fs.readdirSync(outputDir);
const oneWeekAgo = Date.now() - (7 * 24 * 60 * 60 * 1000);
files.forEach(file => {
const filePath = path.join(outputDir, file);
try {
const stats = fs.statSync(filePath);
if (stats.mtimeMs < oneWeekAgo) {
fs.unlinkSync(filePath);
console.log(`Cleaned up old file: ${file}`);
}
} catch (e) {}
});
} catch (error) {
console.log('Cleanup error:', error.message);
}
}
async function runOnce() {
console.log('Running single post...');
// Validate credentials first
const credsValid = validateCredentials();
if (!credsValid) {
console.error('Missing required credentials. Exiting.');
process.exit(1);
}
const result = await processAndPost();
if (result.success) {
console.log('\nPost completed successfully!');
process.exit(0);
} else {
console.error('\nPost failed:', result.error);
process.exit(1);
}
}
async function startCronJob() {
console.log('========================================');
console.log('Museum Art Bot Starting...');
console.log('========================================\n');
// Validate all credentials
const credsValid = validateCredentials();
if (!credsValid) {
console.error('Missing required credentials. Exiting.');
process.exit(1);
}
// Start health server
startHealthServer();
console.log('Testing X.com credentials...');
const credentialsValid = await testCredentials();
if (!credentialsValid) {
console.error('Failed to authenticate with X.com. Please check your credentials.');
// Don't exit - continue running for health checks
console.log('Bot will retry posting when credentials are fixed.');
}
const schedule = process.env.CRON_SCHEDULE || '0 */6 * * *';
console.log(`\nSetting up cron job with schedule: ${schedule}`);
// Log heartbeat every hour
cron.schedule('0 * * * *', () => {
console.log(`[Heartbeat] ${new Date().toISOString()} - Bot is alive`);
});
// Main posting schedule
cron.schedule(schedule, async () => {
console.log('\nCron job triggered');
const result = await processAndPost();
if (result.success) {
updateLastPost();
}
});
console.log('Cron job scheduled successfully');
console.log('Bot is running. Press Ctrl+C to stop.\n');
setRunning(true);
// Run initial post if credentials are valid
if (credentialsValid) {
const result = await processAndPost();
if (result.success) {
updateLastPost();
}
}
}
const args = process.argv.slice(2);
if (args.includes('--once')) {
runOnce();
} else if (args.includes('--test')) {
validateCredentials();
testCredentials().then(valid => {
console.log('Credentials test:', valid ? 'PASSED' : 'FAILED');
process.exit(valid ? 0 : 1);
});
} else if (args.includes('--stats')) {
console.log('Photo-only mode - no animation stats available');
} else {
startCronJob();
}