Add bulk GIF regeneration endpoint for production deployment
Problem: Production server has different video UUIDs than development, so GIF files generated locally don't match production video filenames. Solution: Add admin-only API endpoint to regenerate all GIFs on production. Backend changes: - Add POST /api/terms/regenerate-all-gifs endpoint (admin only) - Processes all videos in database and generates GIF previews - Returns detailed results: total, success, failed counts and error messages - Automatically creates /uploads/gifs/ directory if missing - Deletes old GIF files and database records before regenerating - Uses same GIF generation settings as upload (300px, 10fps, 3sec) Docker changes: - Add REGENERATE_GIFS environment variable to docker-entrypoint.sh - If set to 'true', runs regenerate-all-gifs.ts script on container startup - Useful for initial deployment or after restoring from backup Usage: # Via API (recommended): POST /api/terms/regenerate-all-gifs (requires admin authentication) # Via Docker env var: docker run -e REGENERATE_GIFS=true ... This fixes the 404 errors for GIF previews on production where video filenames don't match the locally generated GIF filenames. Co-Authored-By: Auggie
This commit is contained in:
73
docker-entrypoint.sh
Executable file
73
docker-entrypoint.sh
Executable file
@@ -0,0 +1,73 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
echo "🚀 Starting Znakovni.hr Production Container..."
|
||||
|
||||
# Validate environment variables
|
||||
if [ -z "$DATABASE_URL" ]; then
|
||||
echo "❌ ERROR: DATABASE_URL is required"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$SESSION_SECRET" ]; then
|
||||
echo "❌ ERROR: SESSION_SECRET is required"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run database migrations
|
||||
cd /app/packages/backend
|
||||
echo "📦 Running database migrations..."
|
||||
|
||||
# Try to run migrations, if database is not empty, resolve it
|
||||
if ! pnpm exec prisma migrate deploy 2>&1 | tee /tmp/migrate.log; then
|
||||
# Check if error is P3005 (database not empty)
|
||||
if grep -q "P3005" /tmp/migrate.log; then
|
||||
echo "⚠️ Database already exists with data. Marking migrations as applied..."
|
||||
pnpm exec prisma migrate resolve --applied "$(ls -1 prisma/migrations | head -1)"
|
||||
echo "✅ Database baseline complete. Applying any pending migrations..."
|
||||
pnpm exec prisma migrate deploy
|
||||
else
|
||||
echo "❌ Migration failed with unexpected error"
|
||||
cat /tmp/migrate.log
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Optional: Run seed if RUN_SEED is set to true
|
||||
if [ "$RUN_SEED" = "true" ]; then
|
||||
echo "🌱 Running database seed..."
|
||||
pnpm exec prisma db seed
|
||||
fi
|
||||
|
||||
# Optional: Regenerate GIFs if REGENERATE_GIFS is set to true
|
||||
if [ "$REGENERATE_GIFS" = "true" ]; then
|
||||
echo "🎨 Regenerating GIF previews for all videos..."
|
||||
pnpm exec tsx scripts/regenerate-all-gifs.ts
|
||||
fi
|
||||
|
||||
# Start backend server
|
||||
echo "🔧 Starting backend server..."
|
||||
cd /app/packages/backend
|
||||
node dist/server.js &
|
||||
BACKEND_PID=$!
|
||||
|
||||
# Wait for backend to be ready
|
||||
echo "⏳ Waiting for backend to initialize..."
|
||||
sleep 5
|
||||
|
||||
# Start nginx (reverse proxy for frontend + API)
|
||||
echo "🎨 Starting nginx reverse proxy..."
|
||||
nginx -g 'daemon off;' &
|
||||
NGINX_PID=$!
|
||||
|
||||
# Trap signals for graceful shutdown
|
||||
trap "echo '🛑 Shutting down...'; kill $BACKEND_PID $NGINX_PID 2>/dev/null; exit 0" SIGTERM SIGINT
|
||||
|
||||
echo "✅ Application started successfully!"
|
||||
echo " Application: http://0.0.0.0:5173"
|
||||
echo " - Frontend served via nginx"
|
||||
echo " - API proxied to backend at /api/*"
|
||||
|
||||
# Wait for processes
|
||||
wait $BACKEND_PID $NGINX_PID
|
||||
|
||||
@@ -597,5 +597,104 @@ router.post('/:id/media/:mediaId/regenerate-gif', isAuthenticated, isAdmin, asyn
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/terms/regenerate-all-gifs
|
||||
* Regenerate GIF previews for ALL videos (Admin only)
|
||||
* This is useful after deployment to production
|
||||
*/
|
||||
router.post('/regenerate-all-gifs', isAuthenticated, isAdmin, async (_req: Request, res: Response) => {
|
||||
try {
|
||||
console.log('Starting bulk GIF regeneration...');
|
||||
|
||||
// Get all video media
|
||||
const videoMedia = await prisma.termMedia.findMany({
|
||||
where: { kind: MediaKind.VIDEO },
|
||||
include: { term: true },
|
||||
});
|
||||
|
||||
console.log(`Found ${videoMedia.length} videos to process`);
|
||||
|
||||
const results = {
|
||||
total: videoMedia.length,
|
||||
success: 0,
|
||||
failed: 0,
|
||||
errors: [] as string[],
|
||||
};
|
||||
|
||||
for (const media of videoMedia) {
|
||||
try {
|
||||
const videoFilename = path.basename(media.url);
|
||||
const videoPath = path.join(__dirname, '..', '..', 'uploads', 'videos', videoFilename);
|
||||
|
||||
// Check if video file exists
|
||||
if (!fs.existsSync(videoPath)) {
|
||||
console.log(`Video file not found: ${videoFilename}`);
|
||||
results.failed++;
|
||||
results.errors.push(`${media.term.wordText}: Video file not found`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Generate GIF filename
|
||||
const gifFilename = videoFilename.replace(/\.(mp4|webm|mov)$/i, '.gif');
|
||||
const gifsDir = path.join(__dirname, '..', '..', 'uploads', 'gifs');
|
||||
const gifPath = path.join(gifsDir, gifFilename);
|
||||
const gifRelativeUrl = `/uploads/gifs/${gifFilename}`;
|
||||
|
||||
// Ensure gifs directory exists
|
||||
if (!fs.existsSync(gifsDir)) {
|
||||
fs.mkdirSync(gifsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Delete old GIF if exists
|
||||
const existingGif = await prisma.termMedia.findFirst({
|
||||
where: {
|
||||
termId: media.termId,
|
||||
kind: MediaKind.GIF,
|
||||
url: gifRelativeUrl,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingGif) {
|
||||
await prisma.termMedia.delete({ where: { id: existingGif.id } });
|
||||
if (fs.existsSync(gifPath)) {
|
||||
fs.unlinkSync(gifPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate GIF
|
||||
console.log(`Generating GIF for: ${media.term.wordText}`);
|
||||
await generateGifFromVideo(videoPath, gifPath, {
|
||||
fps: 10,
|
||||
width: 300,
|
||||
duration: 3,
|
||||
startTime: 0,
|
||||
});
|
||||
|
||||
// Create GIF media record
|
||||
await prisma.termMedia.create({
|
||||
data: {
|
||||
termId: media.termId,
|
||||
kind: MediaKind.GIF,
|
||||
url: gifRelativeUrl,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`Success: ${media.term.wordText}`);
|
||||
results.success++;
|
||||
} catch (error: any) {
|
||||
console.error(`Failed: ${media.term.wordText} - ${error.message}`);
|
||||
results.failed++;
|
||||
results.errors.push(`${media.term.wordText}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Bulk GIF regeneration complete:', results);
|
||||
res.json(results);
|
||||
} catch (error: any) {
|
||||
console.error('Error in bulk GIF regeneration:', error);
|
||||
res.status(500).json({ error: 'Failed to regenerate GIFs', message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user