Initial commit: party-mix-app with prefetch cache, audio preload optimizations
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
184
server.js
Normal file
184
server.js
Normal file
@@ -0,0 +1,184 @@
|
||||
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`);
|
||||
});
|
||||
Reference in New Issue
Block a user