const http = require('http'); const https = require('https'); const fs = require('fs'); const path = require('path'); const url = require('url'); const DATA_DIR = process.env.DATA_DIR || './data'; const MUSIC_DIR = process.env.MUSIC_DIR || './music'; const PORT = process.env.PORT || 3001; // Ensure directories exist if (!fs.existsSync(DATA_DIR)) { fs.mkdirSync(DATA_DIR, { recursive: true }); } 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; // 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; if (!token) { res.writeHead(401, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Token required' })); return; } // Simple file-based storage: data/{token}_{key}.json // Using token in filename ensures isolation between users const safeToken = token.replace(/[^a-zA-Z0-9]/g, ''); const filePath = path.join(DATA_DIR, `${safeToken}_${key}.json`); if (req.method === 'GET') { if (fs.existsSync(filePath)) { const data = fs.readFileSync(filePath); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(data); } else { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(null)); // Key not found returns null } } else if (req.method === 'POST') { let body = ''; req.on('data', chunk => body += chunk.toString()); req.on('end', () => { try { // Validate JSON const data = JSON.parse(body); fs.writeFileSync(filePath, body); // Try to handle background download if data looks like a song list // Execute in background tryDownloadSongs(data).catch(err => console.error('Download Manager Error:', err)); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true })); } catch (e) { console.error(e); res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Invalid JSON' })); } }); } else { res.writeHead(405); res.end(); } }); server.listen(PORT, () => { console.log(`Sync Server running on port ${PORT}`); console.log(`Data directory: ${DATA_DIR}`); console.log(`Music directory: ${MUSIC_DIR}`); }); // --- Download Manager --- async function tryDownloadSongs(data) { // Detect if data is a song list // Must be an array, and items must have id, name, and (source or platform) if (!Array.isArray(data) || data.length === 0) return; const isSongList = data.every(item => item && typeof item === 'object' && item.id && item.name && (item.source || item.platform) ); if (!isSongList) return; console.log(`[Sync] Detected song list. Starting background processing for ${data.length} songs...`); for (const song of data) { try { await processSong(song); } catch (e) { console.error(`[Sync] Error processing ${song.name}:`, e.message); } } } function processSong(song) { return new Promise((resolve) => { // Basic sanitization const safeName = (song.name || 'unknown').replace(/[\\/:*?"<>|]/g, '_'); const safeArtist = (song.artist || 'unknown').replace(/[\\/:*?"<>|]/g, '_'); const source = song.platform || song.source; // Filename: Artist - Name [source_id] const baseName = `${safeArtist} - ${safeName} [${source}_${song.id}]`; // Check if file exists (fuzzy match for extension) try { const files = fs.readdirSync(MUSIC_DIR); // Check for standard format OR manual rename (Song Name.ext) const exists = files.some(f => { // 1. Standard format: Artist - Name [source_id].ext if (f.startsWith(baseName)) return true; // 2. Manual rename: Name.ext (ignoring extension) // We check if the file starts with the song name followed by a dot if (f.startsWith(`${safeName}.`)) return true; return false; }); if (exists) { resolve(); return; } } catch (e) { console.error('Error reading music dir:', e); } // Get URL (prefer FLAC) const apiUrl = `https://music-dl.sayqz.com/api/?source=${source}&id=${song.id}&type=url&br=flac`; https.get(apiUrl, (res) => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => { try { const apiRes = JSON.parse(data); const url = apiRes.url; if (!url) { console.log(`[Sync] No URL found for ${song.name}`); resolve(); return; } // Determine extension let ext = 'mp3'; if (url.includes('.flac')) ext = 'flac'; else if (url.includes('.m4a')) ext = 'm4a'; else if (url.includes('.ogg')) ext = 'ogg'; else if (url.includes('.wav')) ext = 'wav'; const dest = path.join(MUSIC_DIR, `${baseName}.${ext}`); downloadFile(url, dest) .then(() => { console.log(`[Sync] Downloaded: ${baseName}.${ext}`); resolve(); }) .catch(err => { console.error(`[Sync] Download failed for ${song.name}:`, err.message); resolve(); }); } catch (e) { console.error('[Sync] API Parse Error:', e); resolve(); } }); }).on('error', (e) => { console.error(`[Sync] API Request Error: ${e.message}`); resolve(); }); }); } function downloadFile(url, dest) { return new Promise((resolve, reject) => { const protocol = url.startsWith('https') ? https : http; protocol.get(url, (res) => { // Handle Redirects if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { downloadFile(res.headers.location, dest).then(resolve).catch(reject); return; } if (res.statusCode !== 200) { reject(new Error(`Status code ${res.statusCode}`)); return; } const file = fs.createWriteStream(dest); res.pipe(file); file.on('finish', () => { file.close(() => resolve()); }); file.on('error', (err) => { fs.unlink(dest, () => {}); reject(err); }); }).on('error', (err) => { fs.unlink(dest, () => {}); reject(err); }); }); }