diff --git a/sync-server/server.js b/sync-server/server.js index 0e5926f..027ecb9 100644 --- a/sync-server/server.js +++ b/sync-server/server.js @@ -17,39 +17,39 @@ if (!fs.existsSync(MUSIC_DIR)) { fs.mkdirSync(MUSIC_DIR, { recursive: true }); } -const server = http.createServer((req, res) => { - // CORS headers - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); - - if (req.method === 'OPTIONS') { - res.writeHead(204); - res.end(); - return; - } - - const parsedUrl = url.parse(req.url, true); - const pathname = parsedUrl.pathname; - - // --- Music API Proxy --- - // Proxy requests to music-dl.sayqz.com to avoid CORS issues - if (pathname === '/music-api' || pathname === '/music-api/') { - const queryString = parsedUrl.search || ''; - const targetUrl = `https://music-dl.sayqz.com/api/${queryString}`; - - proxyRequest(targetUrl, req, res); - return; - } - - // Path format: /kv/:key?token=... - const match = pathname.match(/^\/kv\/([a-zA-Z0-9_-]+)$/); - - if (!match) { - res.writeHead(404, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: 'Invalid path' })); - return; - } +const server = http.createServer((req, res) => { + // CORS headers + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + + if (req.method === 'OPTIONS') { + res.writeHead(204); + res.end(); + return; + } + + const parsedUrl = url.parse(req.url, true); + const pathname = parsedUrl.pathname; + + // --- Music API Proxy --- + // Proxy requests to music-dl.sayqz.com to avoid CORS issues + // if (pathname === '/music-api' || pathname === '/music-api/') { + // const queryString = parsedUrl.search || ''; + // const targetUrl = `https://music-dl.sayqz.com/api/${queryString}`; + + // proxyRequest(targetUrl, req, res); + // return; + // } + + // Path format: /kv/:key?token=... + const match = pathname.match(/^\/kv\/([a-zA-Z0-9_-]+)$/); + + if (!match) { + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Invalid path' })); + return; + } const key = match[1]; const token = parsedUrl.query.token; @@ -308,224 +308,224 @@ function downloadFile(url, dest) { }); } -// --- Music API Proxy Function --- -function proxyRequest(targetUrl, req, res) { - const parsedTarget = url.parse(targetUrl, true); - const requestType = parsedTarget.query && parsedTarget.query.type; - const isPicRequest = requestType === 'pic'; - const isUrlRequest = requestType === 'url'; - const maxRedirects = 5; - - const buildHeaders = () => { - const 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-Encoding': 'identity' - }; - if (req.headers.range) headers['Range'] = req.headers.range; - if (req.headers['if-range']) headers['If-Range'] = req.headers['if-range']; - return headers; - }; - - const requestStream = (nextUrl, redirectCount = 0) => { - if (redirectCount > maxRedirects) { - res.writeHead(502, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: 'Too many redirects' })); - return; - } - - const parsedNext = url.parse(nextUrl); - const isHttps = parsedNext.protocol === 'https:'; - const requestModule = isHttps ? https : http; - const options = { - hostname: parsedNext.hostname, - port: parsedNext.port || (isHttps ? 443 : 80), - path: parsedNext.path, - method: req.method, - headers: buildHeaders() - }; - - const proxyReq = requestModule.request(options, (proxyRes) => { - if (proxyRes.statusCode >= 300 && proxyRes.statusCode < 400 && proxyRes.headers.location) { - const resolved = url.resolve(nextUrl, proxyRes.headers.location); - proxyRes.resume(); - requestStream(resolved, redirectCount + 1); - return; - } - - const headers = { ...proxyRes.headers }; - delete headers['content-encoding']; - delete headers['transfer-encoding']; - res.writeHead(proxyRes.statusCode || 200, headers); - proxyRes.pipe(res); - }); - - proxyReq.on('error', (e) => { - console.error('[Proxy] Stream error:', e.message); - res.writeHead(502, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: 'Proxy stream failed', message: e.message })); - }); - - if (req.method === 'POST') { - req.pipe(proxyReq); - } else { - proxyReq.end(); - } - }; - - const options = { - hostname: parsedTarget.hostname, - port: 443, - path: parsedTarget.path, - method: req.method, - headers: buildHeaders() - }; - - if (isUrlRequest) { - const proxyReq = https.request(options, (proxyRes) => { - if (proxyRes.statusCode >= 300 && proxyRes.statusCode < 400 && proxyRes.headers.location) { - const resolved = url.resolve(targetUrl, proxyRes.headers.location); - proxyRes.resume(); - requestStream(resolved, 1); - return; - } - - const contentType = proxyRes.headers['content-type'] || ''; - const shouldInspectBody = contentType.includes('application/json') || contentType.startsWith('text/'); - - if (shouldInspectBody) { - let body = ''; - proxyRes.setEncoding('utf8'); - proxyRes.on('data', chunk => body += chunk); - proxyRes.on('end', () => { - try { - const parsedBody = JSON.parse(body); - const extractUrl = (payload) => { - if (!payload || typeof payload !== 'object') return null; - if (typeof payload.url === 'string') return payload.url; - if (typeof payload.data === 'string') return payload.data; - if (payload.data && typeof payload.data === 'object') { - if (typeof payload.data.url === 'string') return payload.data.url; - if (typeof payload.data.link === 'string') return payload.data.link; - if (payload.data.data && typeof payload.data.data.url === 'string') return payload.data.data.url; - if (Array.isArray(payload.data) && payload.data[0] && typeof payload.data[0].url === 'string') { - return payload.data[0].url; - } - } - if (payload.result && typeof payload.result.url === 'string') return payload.result.url; - return null; - }; - const resolvedUrl = extractUrl(parsedBody); - if (resolvedUrl) { - requestStream(url.resolve(targetUrl, resolvedUrl), 1); - return; - } - } catch (e) { - // fall through to return original body - } - - res.writeHead(proxyRes.statusCode || 200, { - 'Content-Type': proxyRes.headers['content-type'] || 'application/json' - }); - res.end(body); - }); - return; - } - - const headers = { ...proxyRes.headers }; - delete headers['content-encoding']; - delete headers['transfer-encoding']; - res.writeHead(proxyRes.statusCode || 200, headers); - proxyRes.pipe(res); - }); - - proxyReq.on('error', (e) => { - console.error('[Proxy] Request error:', e.message); - res.writeHead(502, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: 'Proxy request failed', message: e.message })); - }); - - if (req.method === 'POST') { - req.pipe(proxyReq); - } else { - proxyReq.end(); - } - return; - } - - const proxyReq = https.request(options, (proxyRes) => { - const passThrough = () => { - const headers = { ...proxyRes.headers }; - delete headers['content-encoding']; - delete headers['transfer-encoding']; - - res.writeHead(proxyRes.statusCode, headers); - proxyRes.pipe(res); - }; - - // pic endpoint now returns JSON, turn it into a redirect for - if (isPicRequest) { - if (proxyRes.statusCode >= 300 && proxyRes.statusCode < 400 && proxyRes.headers.location) { - res.writeHead(302, { Location: proxyRes.headers.location }); - proxyRes.resume(); - res.end(); - return; - } - - const contentType = proxyRes.headers['content-type'] || ''; - if (contentType.startsWith('image/')) { - passThrough(); - return; - } - - let body = ''; - proxyRes.setEncoding('utf8'); - proxyRes.on('data', chunk => body += chunk); - proxyRes.on('end', () => { - try { - const parsedBody = JSON.parse(body); - if (parsedBody && parsedBody.url) { - res.writeHead(302, { Location: parsedBody.url }); - res.end(); - return; - } - } catch (e) { - // fall through - } - - res.writeHead(proxyRes.statusCode || 200, { - 'Content-Type': proxyRes.headers['content-type'] || 'application/json' - }); - res.end(body); - }); - return; - } - - // Handle redirects - if (proxyRes.statusCode >= 300 && proxyRes.statusCode < 400 && proxyRes.headers.location) { - // For redirects, return the redirect URL to client - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ url: proxyRes.headers.location })); - return; - } - - passThrough(); - }); - - proxyReq.on('error', (e) => { - console.error('[Proxy] Request error:', e.message); - res.writeHead(502, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: 'Proxy request failed', message: e.message })); - }); - - // Forward request body for POST requests - if (req.method === 'POST') { - req.pipe(proxyReq); - } else { - proxyReq.end(); - } -} - +// --- Music API Proxy Function --- +function proxyRequest(targetUrl, req, res) { + const parsedTarget = url.parse(targetUrl, true); + const requestType = parsedTarget.query && parsedTarget.query.type; + const isPicRequest = requestType === 'pic'; + const isUrlRequest = requestType === 'url'; + const maxRedirects = 5; + + const buildHeaders = () => { + const 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-Encoding': 'identity' + }; + if (req.headers.range) headers['Range'] = req.headers.range; + if (req.headers['if-range']) headers['If-Range'] = req.headers['if-range']; + return headers; + }; + + const requestStream = (nextUrl, redirectCount = 0) => { + if (redirectCount > maxRedirects) { + res.writeHead(502, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Too many redirects' })); + return; + } + + const parsedNext = url.parse(nextUrl); + const isHttps = parsedNext.protocol === 'https:'; + const requestModule = isHttps ? https : http; + const options = { + hostname: parsedNext.hostname, + port: parsedNext.port || (isHttps ? 443 : 80), + path: parsedNext.path, + method: req.method, + headers: buildHeaders() + }; + + const proxyReq = requestModule.request(options, (proxyRes) => { + if (proxyRes.statusCode >= 300 && proxyRes.statusCode < 400 && proxyRes.headers.location) { + const resolved = url.resolve(nextUrl, proxyRes.headers.location); + proxyRes.resume(); + requestStream(resolved, redirectCount + 1); + return; + } + + const headers = { ...proxyRes.headers }; + delete headers['content-encoding']; + delete headers['transfer-encoding']; + res.writeHead(proxyRes.statusCode || 200, headers); + proxyRes.pipe(res); + }); + + proxyReq.on('error', (e) => { + console.error('[Proxy] Stream error:', e.message); + res.writeHead(502, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Proxy stream failed', message: e.message })); + }); + + if (req.method === 'POST') { + req.pipe(proxyReq); + } else { + proxyReq.end(); + } + }; + + const options = { + hostname: parsedTarget.hostname, + port: 443, + path: parsedTarget.path, + method: req.method, + headers: buildHeaders() + }; + + if (isUrlRequest) { + const proxyReq = https.request(options, (proxyRes) => { + if (proxyRes.statusCode >= 300 && proxyRes.statusCode < 400 && proxyRes.headers.location) { + const resolved = url.resolve(targetUrl, proxyRes.headers.location); + proxyRes.resume(); + requestStream(resolved, 1); + return; + } + + const contentType = proxyRes.headers['content-type'] || ''; + const shouldInspectBody = contentType.includes('application/json') || contentType.startsWith('text/'); + + if (shouldInspectBody) { + let body = ''; + proxyRes.setEncoding('utf8'); + proxyRes.on('data', chunk => body += chunk); + proxyRes.on('end', () => { + try { + const parsedBody = JSON.parse(body); + const extractUrl = (payload) => { + if (!payload || typeof payload !== 'object') return null; + if (typeof payload.url === 'string') return payload.url; + if (typeof payload.data === 'string') return payload.data; + if (payload.data && typeof payload.data === 'object') { + if (typeof payload.data.url === 'string') return payload.data.url; + if (typeof payload.data.link === 'string') return payload.data.link; + if (payload.data.data && typeof payload.data.data.url === 'string') return payload.data.data.url; + if (Array.isArray(payload.data) && payload.data[0] && typeof payload.data[0].url === 'string') { + return payload.data[0].url; + } + } + if (payload.result && typeof payload.result.url === 'string') return payload.result.url; + return null; + }; + const resolvedUrl = extractUrl(parsedBody); + if (resolvedUrl) { + requestStream(url.resolve(targetUrl, resolvedUrl), 1); + return; + } + } catch (e) { + // fall through to return original body + } + + res.writeHead(proxyRes.statusCode || 200, { + 'Content-Type': proxyRes.headers['content-type'] || 'application/json' + }); + res.end(body); + }); + return; + } + + const headers = { ...proxyRes.headers }; + delete headers['content-encoding']; + delete headers['transfer-encoding']; + res.writeHead(proxyRes.statusCode || 200, headers); + proxyRes.pipe(res); + }); + + proxyReq.on('error', (e) => { + console.error('[Proxy] Request error:', e.message); + res.writeHead(502, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Proxy request failed', message: e.message })); + }); + + if (req.method === 'POST') { + req.pipe(proxyReq); + } else { + proxyReq.end(); + } + return; + } + + const proxyReq = https.request(options, (proxyRes) => { + const passThrough = () => { + const headers = { ...proxyRes.headers }; + delete headers['content-encoding']; + delete headers['transfer-encoding']; + + res.writeHead(proxyRes.statusCode, headers); + proxyRes.pipe(res); + }; + + // pic endpoint now returns JSON, turn it into a redirect for + if (isPicRequest) { + if (proxyRes.statusCode >= 300 && proxyRes.statusCode < 400 && proxyRes.headers.location) { + res.writeHead(302, { Location: proxyRes.headers.location }); + proxyRes.resume(); + res.end(); + return; + } + + const contentType = proxyRes.headers['content-type'] || ''; + if (contentType.startsWith('image/')) { + passThrough(); + return; + } + + let body = ''; + proxyRes.setEncoding('utf8'); + proxyRes.on('data', chunk => body += chunk); + proxyRes.on('end', () => { + try { + const parsedBody = JSON.parse(body); + if (parsedBody && parsedBody.url) { + res.writeHead(302, { Location: parsedBody.url }); + res.end(); + return; + } + } catch (e) { + // fall through + } + + res.writeHead(proxyRes.statusCode || 200, { + 'Content-Type': proxyRes.headers['content-type'] || 'application/json' + }); + res.end(body); + }); + return; + } + + // Handle redirects + if (proxyRes.statusCode >= 300 && proxyRes.statusCode < 400 && proxyRes.headers.location) { + // For redirects, return the redirect URL to client + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ url: proxyRes.headers.location })); + return; + } + + passThrough(); + }); + + proxyReq.on('error', (e) => { + console.error('[Proxy] Request error:', e.message); + res.writeHead(502, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Proxy request failed', message: e.message })); + }); + + // Forward request body for POST requests + if (req.method === 'POST') { + req.pipe(proxyReq); + } else { + proxyReq.end(); + } +} + function writeMetadata(inputPath, outputPath, metadata) { return new Promise((resolve, reject) => { const args = ['-i', inputPath]; @@ -585,4 +585,4 @@ function writeMetadata(inputPath, outputPath, metadata) { reject(err); }); }); -} +}