import 'dotenv/config'; import express from 'express'; import cors from 'cors'; import helmet from 'helmet'; import morgan from 'morgan'; import fs from 'fs'; import path from 'path'; import https from 'https'; import { db } from './lib/db.js'; import { authRouter } from './routes/auth.js'; import { usersRouter } from './routes/users.js'; import { partnersRouter } from './routes/partners.js'; import { playlistsRouter } from './routes/playlists.js'; import { syncUserData, captureNowPlaying } from './lib/sync.js'; const app = express(); app.use(helmet()); app.use(cors({ origin: [ 'http://localhost:4000', 'https://159.195.9.107:3443', 'http://159.195.9.107:3443' ], credentials: true })); // Add CORS headers to allow mixed content for development app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization'); next(); }); app.use(express.json({ limit: '1mb' })); app.use(express.urlencoded({ extended: false })); app.use(morgan('dev')); // Serve static files from public directory app.use(express.static('public')); // Initialize DB db.initialize(); app.get('/health', (_req, res) => { res.json({ ok: true }); }); // Placeholder playlist cover endpoint app.get('/api/placeholder-playlist-cover', (_req, res) => { // Return a simple SVG placeholder const svg = ` 🎵 `; res.setHeader('Content-Type', 'image/svg+xml'); res.send(svg); }); // Serve playlist cover images as base64 data URLs to avoid mixed content issues app.get('/api/playlist-covers/:filename', (req, res) => { try { const filename = decodeURIComponent(req.params.filename); const filePath = path.join(process.cwd(), 'public', 'playlist-covers', filename); // Check if file exists if (!fs.existsSync(filePath)) { return res.status(404).json({ error: 'Image not found' }); } // Read file as base64 const fileBuffer = fs.readFileSync(filePath); const base64Data = fileBuffer.toString('base64'); // Set appropriate content type based on file extension const ext = path.extname(filename).toLowerCase(); let contentType = 'image/jpeg'; // default switch (ext) { case '.png': contentType = 'image/png'; break; case '.gif': contentType = 'image/gif'; break; case '.webp': contentType = 'image/webp'; break; case '.svg': contentType = 'image/svg+xml'; break; case '.jpg': case '.jpeg': contentType = 'image/jpeg'; break; } // Return base64 data URL const dataUrl = `data:${contentType};base64,${base64Data}`; res.setHeader('Content-Type', 'application/json'); res.setHeader('Cache-Control', 'public, max-age=31536000'); // Cache for 1 year res.json({ dataUrl }); } catch (error) { console.error('Error serving playlist cover:', error); res.status(500).json({ error: 'Failed to serve image' }); } }); app.use('/auth', authRouter); app.use('/users', usersRouter); app.use('/partners', partnersRouter); app.use('/playlists', playlistsRouter); const PORT = process.env.PORT ? Number(process.env.PORT) : 8081; const HTTPS_PORT = process.env.HTTPS_PORT ? Number(process.env.HTTPS_PORT) : 8082; // Start HTTP server app.listen(PORT, () => { console.log(`API server (HTTP) listening on http://0.0.0.0:${PORT}`); }); // Start HTTPS server try { const httpsOptions = { key: fs.readFileSync(path.join(process.cwd(), 'key.pem')), cert: fs.readFileSync(path.join(process.cwd(), 'cert.pem')) }; https.createServer(httpsOptions, app).listen(HTTPS_PORT, () => { console.log(`API server (HTTPS) listening on https://0.0.0.0:${HTTPS_PORT}`); }); } catch (error) { console.log('HTTPS server not started - SSL certificates not found'); } // Periodic refreshes const REFRESH_INTERVAL_MS = 2 * 60 * 1000; // full sync const NOWPLAYING_INTERVAL_MS = 20 * 1000; // capture now playing setInterval(() => { try { const users = db.db.prepare('SELECT id FROM users').all(); for (const u of users) { syncUserData(u.id).catch(() => void 0); } } catch (e) { // ignore timer errors } }, REFRESH_INTERVAL_MS); setInterval(() => { try { const users = db.db.prepare('SELECT id FROM users').all(); for (const u of users) captureNowPlaying(u.id).catch(() => void 0); } catch { } }, NOWPLAYING_INTERVAL_MS);