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:
2026-04-25 12:40:22 +03:00
commit 0097fb5183
83 changed files with 11788 additions and 0 deletions

184
server.js Normal file
View 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`);
});