const http = require('http'); const https = require('https'); const fs = require('fs'); const path = require('path'); const url = require('url'); const PORT = 3000; function proxyRequest(targetUrl, res, extraHeaders = {}) { const parsedTarget = new URL(targetUrl); const options = { hostname: parsedTarget.hostname, path: parsedTarget.pathname + parsedTarget.search, method: 'GET', headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Accept': '*/*', 'Accept-Language': 'ru-RU,ru;q=0.9', 'Referer': 'https://rus.hitmotop.com/', 'Origin': 'https://rus.hitmotop.com', ...extraHeaders } }; const req = https.request(options, (proxyRes) => { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Headers', '*'); // Пробрасываем важные заголовки const ct = proxyRes.headers['content-type']; if (ct) res.setHeader('Content-Type', ct); const cl = proxyRes.headers['content-length']; if (cl) res.setHeader('Content-Length', cl); const cr = proxyRes.headers['content-range']; if (cr) res.setHeader('Content-Range', cr); const ar = proxyRes.headers['accept-ranges']; if (ar) res.setHeader('Accept-Ranges', ar); res.statusCode = proxyRes.statusCode; proxyRes.pipe(res); }); req.on('error', (e) => { res.statusCode = 500; res.end('Proxy error: ' + e.message); }); req.end(); } const server = http.createServer((req, res) => { const parsed = url.parse(req.url, true); res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Headers', '*'); if (req.method === 'OPTIONS') { res.end(); return; } // /img?url=... — проксируем обложки альбомов if (parsed.pathname === '/img') { const imgurl = parsed.query.url || ''; if (!imgurl.startsWith('http')) { res.statusCode = 400; res.end('Bad url'); return; } const pu = new URL(imgurl); const reqOpts = { hostname: pu.hostname, path: pu.pathname + pu.search, method: 'GET', headers: { 'User-Agent': 'Mozilla/5.0', 'Referer': 'https://rus.hitmotop.com/', } }; const pr = https.request(reqOpts, (proxyRes) => { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Cache-Control', 'public, max-age=86400'); const ct = proxyRes.headers['content-type'] || 'image/jpeg'; res.setHeader('Content-Type', ct); res.statusCode = proxyRes.statusCode; proxyRes.pipe(res); }); pr.on('error', () => { res.statusCode = 500; res.end(); }); pr.end(); return; } // /search?q=... — поиск треков if (parsed.pathname === '/search') { const q = parsed.query.q || ''; proxyRequest(`https://rus.hitmotop.com/search?q=${encodeURIComponent(q)}`, res); return; } // /mp3?url=... — проксируем MP3 файл с правильным Referer if (parsed.pathname === '/mp3') { const mp3url = parsed.query.url || ''; console.log(`\n[MP3] Запрос: ${mp3url}`); if (!mp3url.startsWith('https://rus.hitmotop.com/') && !mp3url.startsWith('https://hitmotop.com/')) { console.log('[MP3] ОТКЛОНЁН — не hitmotop URL'); res.statusCode = 403; res.end('Only hitmotop URLs allowed'); return; } // Поддержка Range requests для перемотки const rangeHeader = req.headers['range']; if (rangeHeader) console.log(`[MP3] Range: ${rangeHeader}`); function fetchMp3(targetUrl, redirectCount) { if (redirectCount > 5) { console.log('[MP3] Слишком много редиректов!'); res.statusCode = 500; res.end('Too many redirects'); return; } const parsedUrl = new URL(targetUrl); const reqOptions = { hostname: parsedUrl.hostname, path: parsedUrl.pathname + parsedUrl.search, method: 'GET', headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Accept': 'audio/mpeg, audio/*, */*', 'Accept-Language': 'ru-RU,ru;q=0.9', 'Referer': 'https://rus.hitmotop.com/', 'Origin': 'https://rus.hitmotop.com', ...(rangeHeader ? { 'Range': rangeHeader } : {}) } }; const proxyReq = https.request(reqOptions, (proxyRes) => { console.log(`[MP3] HTTP ${proxyRes.statusCode} ← ${targetUrl.substring(0,80)}`); console.log(`[MP3] Content-Type: ${proxyRes.headers['content-type']}`); // Следуем за редиректом if ((proxyRes.statusCode === 301 || proxyRes.statusCode === 302 || proxyRes.statusCode === 307 || proxyRes.statusCode === 308) && proxyRes.headers['location']) { const location = proxyRes.headers['location']; const nextUrl = location.startsWith('http') ? location : `https://${parsedUrl.hostname}${location}`; console.log(`[MP3] Редирект → ${nextUrl}`); proxyRes.resume(); // дочитываем тело чтобы освободить соединение fetchMp3(nextUrl, redirectCount + 1); return; } res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Content-Type', 'audio/mpeg'); ['content-length','content-range','accept-ranges'].forEach(h => { if (proxyRes.headers[h]) res.setHeader(h, proxyRes.headers[h]); }); res.statusCode = proxyRes.statusCode; proxyRes.pipe(res); proxyRes.on('end', () => console.log('[MP3] ✓ Передача завершена')); }); proxyReq.on('error', (e) => { console.log(`[MP3] ОШИБКА: ${e.message}`); if (!res.headersSent) { res.statusCode = 500; res.end('Proxy error: ' + e.message); } }); proxyReq.end(); } fetchMp3(mp3url, 0); return; } // / или /index.html — отдаём приложение if (parsed.pathname === '/' || parsed.pathname === '/index.html') { const filePath = path.join(__dirname, 'index.html'); fs.readFile(filePath, (err, data) => { if (err) { res.statusCode = 404; res.end('Not found'); return; } res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.end(data); }); return; } res.statusCode = 404; res.end('Not found'); }); server.listen(PORT, () => { console.log('\n\x1b[32m🎉 Party Mix запущен!\x1b[0m'); console.log(`\nОткройте в браузере: \x1b[36mhttp://localhost:${PORT}\x1b[0m\n`); });