146 lines
5.3 KiB
JavaScript
146 lines
5.3 KiB
JavaScript
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 = `
|
|
<svg width="300" height="300" xmlns="http://www.w3.org/2000/svg">
|
|
<defs>
|
|
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
<stop offset="0%" style="stop-color:#8B5CF6;stop-opacity:1" />
|
|
<stop offset="100%" style="stop-color:#EC4899;stop-opacity:1" />
|
|
</linearGradient>
|
|
</defs>
|
|
<rect width="300" height="300" fill="url(#grad1)"/>
|
|
<text x="150" y="150" font-family="Arial, sans-serif" font-size="24" fill="white" text-anchor="middle" dominant-baseline="middle">🎵</text>
|
|
</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);
|