← 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();
}