const express = require('express'); const http = require('http'); const socketIo = require('socket.io'); const cors = require('cors'); const path = require('path'); const multer = require('multer'); const { v4: uuidv4 } = require('uuid'); const fs = require('fs-extra'); const DatabaseManager = require('./database/DatabaseManager'); const ContentManager = require('./services/ContentManager'); const ScheduleManager = require('./services/ScheduleManager'); const app = express(); const server = http.createServer(app); const io = socketIo(server, { cors: { origin: "*", methods: ["GET", "POST"] } }); const PORT = process.env.PORT || 3000; // Middleware app.use(cors()); app.use(express.json()); app.use(express.static(path.join(__dirname, '../public'))); // File upload configuration const storage = multer.diskStorage({ destination: (req, file, cb) => { let uploadPath; if (file.mimetype.startsWith('image/')) { uploadPath = path.join(__dirname, '../public/uploads/images'); } else if (file.mimetype.startsWith('video/')) { uploadPath = path.join(__dirname, '../public/uploads/videos'); } else { return cb(new Error('Unsupported file type')); } cb(null, uploadPath); }, filename: (req, file, cb) => { const uniqueName = `${uuidv4()}-${file.originalname}`; cb(null, uniqueName); } }); const upload = multer({ storage: storage }); // Initialize managers const dbManager = new DatabaseManager(); const contentManager = new ContentManager(dbManager); const scheduleManager = new ScheduleManager(dbManager, io); // Initialize database dbManager.initialize(); // API Routes // Content Management app.post('/api/content/upload', upload.single('content'), async (req, res) => { try { if (!req.file) { return res.status(400).json({ error: 'No file uploaded' }); } const contentData = { id: uuidv4(), type: req.body.type, title: req.body.title || req.file.originalname, filename: req.file.filename, originalName: req.file.originalname, mimeType: req.file.mimetype, size: req.file.size, path: req.file.path, url: `/uploads/${req.file.mimetype.startsWith('image/') ? 'images' : 'videos'}/${req.file.filename}`, zone: req.body.zone || 'all', duration: parseInt(req.body.duration) || 10, createdAt: new Date().toISOString() }; const content = await contentManager.addContent(contentData); // Emit real-time update io.emit('contentUpdated', { type: 'content_added', content: content }); res.json({ success: true, content }); } catch (error) { console.error('Upload error:', error); res.status(500).json({ error: 'Upload failed' }); } }); // Text Content Management app.post('/api/content/text', async (req, res) => { try { const { title, textContent, zone, duration } = req.body; if (!title || !textContent) { return res.status(400).json({ error: 'Title and text content are required' }); } const contentData = { id: uuidv4(), title: title, textContent: textContent, zone: zone || 'all', duration: parseInt(duration) || 15, createdAt: new Date().toISOString() }; const content = await contentManager.addTextContent(contentData); // Emit real-time update io.emit('contentUpdated', { type: 'content_added', content: content }); res.json({ success: true, content }); } catch (error) { console.error('Text content creation error:', error); res.status(500).json({ error: 'Failed to create text content' }); } }); app.get('/api/content', async (req, res) => { try { const { zone, type } = req.query; const content = await contentManager.getContent(zone, type); res.json(content); } catch (error) { console.error('Get content error:', error); res.status(500).json({ error: 'Failed to retrieve content' }); } }); app.delete('/api/content/:id', async (req, res) => { try { const { id } = req.params; const content = await contentManager.getContentById(id); if (!content) { return res.status(404).json({ error: 'Content not found' }); } // Delete physical file await fs.remove(content.path); // Delete from database await contentManager.deleteContent(id); // Emit real-time update io.emit('contentUpdated', { type: 'content_deleted', contentId: id }); res.json({ success: true }); } catch (error) { console.error('Delete content error:', error); res.status(500).json({ error: 'Failed to delete content' }); } }); // Schedule Management app.post('/api/schedule', async (req, res) => { try { const scheduleData = { id: uuidv4(), contentId: req.body.contentId, zone: req.body.zone, startTime: req.body.startTime, endTime: req.body.endTime, priority: req.body.priority || 1, createdAt: new Date().toISOString() }; const schedule = await scheduleManager.addSchedule(scheduleData); io.emit('scheduleUpdated', { type: 'schedule_added', schedule: schedule }); res.json({ success: true, schedule }); } catch (error) { console.error('Schedule creation error:', error); res.status(500).json({ error: 'Failed to create schedule' }); } }); app.get('/api/schedule/:zone', async (req, res) => { try { const { zone } = req.params; const schedule = await scheduleManager.getActiveSchedule(zone); res.json(schedule); } catch (error) { console.error('Get schedule error:', error); res.status(500).json({ error: 'Failed to retrieve schedule' }); } }); app.get('/api/zones', async (req, res) => { try { const zones = await dbManager.getZones(); res.json(zones); } catch (error) { console.error('Get zones error:', error); res.status(500).json({ error: 'Failed to retrieve zones' }); } }); app.post('/api/zones', async (req, res) => { try { const { id, name, description, icon, displayOrder } = req.body; if (!id || !name) { return res.status(400).json({ error: 'Zone ID and name are required' }); } const zoneData = { id: id.toLowerCase().replace(/\s+/g, '-'), name: name, description: description || '', icon: icon || 'fa-map-marker-alt', displayOrder: parseInt(displayOrder) || 0 }; const zone = await dbManager.addZone(zoneData); io.emit('zonesUpdated', { type: 'zone_added', zone: zone }); res.json({ success: true, zone }); } catch (error) { console.error('Create zone error:', error); res.status(500).json({ error: 'Failed to create zone' }); } }); // Weather widget data app.get('/api/weather', (req, res) => { // Mock weather data - in real implementation, integrate with weather API const weatherData = { temperature: -5, snowCondition: 'Frisse sneeuw', slopeCondition: 'Perfect', humidity: 65, windSpeed: 8, lastUpdated: new Date().toISOString() }; res.json(weatherData); }); // Socket.io connection handling io.on('connection', (socket) => { console.log('Client connected:', socket.id); socket.on('joinZone', (zone) => { socket.join(zone); console.log(`Client ${socket.id} joined zone: ${zone}`); }); socket.on('leaveZone', (zone) => { socket.leave(zone); console.log(`Client ${socket.id} left zone: ${zone}`); }); socket.on('disconnect', () => { console.log('Client disconnected:', socket.id); }); }); // Error handling middleware app.use((error, req, res, next) => { console.error('Server error:', error); res.status(500).json({ error: 'Internal server error', message: process.env.NODE_ENV === 'development' ? error.message : 'Something went wrong' }); }); // 404 handler app.use((req, res) => { res.status(404).json({ error: 'Route not found' }); }); server.listen(PORT, () => { console.log(`SnowWorld Narrowcasting Server running on port ${PORT}`); console.log(`Environment: ${process.env.NODE_ENV || 'development'}`); });