const express = require('express'); const cors = require('cors'); const helmet = require('helmet'); const rateLimit = require('express-rate-limit'); const axios = require('axios'); const yts = require('yt-search'); const path = require('path'); const app = express(); const PORT = process.env.PORT || 3000; // Security HTTP Headers with Strict Referrer Policy (Fixes YouTube Error 153) app.use(helmet({ contentSecurityPolicy: false, crossOriginEmbedderPolicy: false, referrerPolicy: { policy: 'strict-origin-when-cross-origin' } })); // CORS Configuration const allowedOrigins = [ 'http://localhost:3000', 'http://127.0.0.1:3000', 'https://nova-play.vinniedigitalhub.co.ke', 'https://novap-ed1c8d7b15ad.herokuapp.com' ]; app.use(cors({ origin: function (origin, callback) { if (!origin || allowedOrigins.indexOf(origin) !== -1 || process.env.NODE_ENV !== 'production') { callback(null, true); } else { callback(new Error('Blocked by CORS security policy')); } }, methods: ['GET', 'POST'], allowedHeaders: ['Content-Type', 'X-Requested-With', 'X-NovaPlay-Client'] })); app.use(express.json()); app.use(express.static(path.join(__dirname, 'public'))); // Anti-Scraping Guard const antiScrapeGuard = (req, res, next) => { const userAgent = req.headers['user-agent'] || ''; const clientHeader = req.headers['x-novaplay-client']; const botPatterns = /python|curl|wget|scrapy|httpclient|java|go-http-client|php|axios|postman|insomnia|bot|crawler|spider/i; if (botPatterns.test(userAgent) && !clientHeader) { return res.status(403).json({ success: false, error: 'Access denied. Automated scraping is strictly prohibited.' }); } if (req.path.startsWith('/api/') && clientHeader !== 'novaplay-web-v1') { return res.status(403).json({ success: false, error: 'Unauthorized API request.' }); } next(); }; // Rate Limiting Rules (60 requests per 5 mins per IP) const apiLimiter = rateLimit({ windowMs: 5 * 60 * 1000, max: 60, standardHeaders: true, legacyHeaders: false, message: { success: false, error: 'Too many requests. Please wait a few minutes before trying again.' } }); app.use('/api/', apiLimiter); app.use('/api/', antiScrapeGuard); // 1. YouTube Search & Direct URL Handler app.get('/api/search', async (req, res) => { try { const query = req.query.q; if (!query) { return res.status(400).json({ success: false, error: 'Search query is required' }); } const ytRegex = /(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/; const match = query.match(ytRegex); if (match && match[1]) { const videoId = match[1]; try { const singleResult = await yts({ videoId: videoId }); return res.json({ success: true, isDirectUrl: true, results: [{ id: singleResult.videoId, url: singleResult.url, title: singleResult.title, thumbnail: singleResult.thumbnail || singleResult.image, duration: singleResult.timestamp, author: singleResult.author ? singleResult.author.name : 'YouTube Video', views: singleResult.views || 0 }] }); } catch (err) { return res.json({ success: true, isDirectUrl: true, results: [{ id: videoId, url: `https://www.youtube.com/watch?v=${videoId}`, title: `YouTube Video (${videoId})`, thumbnail: `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`, duration: 'N/A', author: 'YouTube', views: 0 }] }); } } const searchResults = await yts(query); const videos = searchResults.videos.slice(0, 16).map(v => ({ id: v.videoId, url: v.url, title: v.title, thumbnail: v.thumbnail || v.image, duration: v.timestamp, author: v.author ? v.author.name : 'Unknown Artist', views: v.views })); res.json({ success: true, isDirectUrl: false, results: videos }); } catch (err) { console.error('Search error:', err.message); res.status(500).json({ success: false, error: 'Failed to process search request' }); } }); // 2. Download Job Initiation app.get('/api/download', async (req, res) => { try { const { url, format } = req.query; if (!url || !format) { return res.status(400).json({ success: false, error: 'URL and format are required parameters' }); } const targetUrl = `https://p.savenow.to/ajax/download.php?url=${encodeURIComponent(url)}&format=${encodeURIComponent(format)}`; const response = await axios.get(targetUrl, { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', 'Referer': 'https://savenow.to/', 'Origin': 'https://savenow.to' }, timeout: 10000 }); res.json(response.data); } catch (err) { console.error('Download initiation error:', err.message); res.status(500).json({ success: false, error: 'Conversion service communication failure' }); } }); // 3. Conversion Progress Check app.get('/api/progress', async (req, res) => { try { const { id } = req.query; if (!id) { return res.status(400).json({ success: false, error: 'Job ID parameter missing' }); } const targetUrl = `https://p.savenow.to/api/progress?id=${encodeURIComponent(id)}`; const response = await axios.get(targetUrl, { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', 'Referer': 'https://savenow.to/', 'Origin': 'https://savenow.to' }, timeout: 10000 }); res.json(response.data); } catch (err) { console.error('Progress check error:', err.message); res.status(500).json({ success: false, error: 'Failed to query task progress' }); } }); app.listen(PORT, () => { console.log(`NovaPlay active on http://localhost:${PORT}`); });