// server/server.js import express from 'express'; import http from 'http'; import cors from 'cors'; import { Server as SocketIOServer } from 'socket.io'; import path from 'path'; import { fileURLToPath } from 'url'; import axios from 'axios'; import NodeCache from 'node-cache'; import { searchUniversal, getSong, getAlbum, getPlaylist, getLyrics } from './jiosaavn.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const app = express(); app.use(cors({ origin: '*', credentials: true })); app.use(express.json()); const vidflyCache = new NodeCache({ stdTTL: 300, checkperiod: 60 }); app.get('/healthz', (_req, res) => res.send('OK')); // Range‑aware proxy with CORS headers app.options('/api/proxy', (_req, res) => { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Headers', 'Range, Content-Type, Accept, Accept-Encoding, Accept-Language'); res.setHeader('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS'); res.setHeader('Access-Control-Allow-Credentials', 'true'); res.status(204).end(); }); app.get('/api/proxy', async (req, res) => { const target = req.query.url; if (!target) return res.status(400).send('Missing url'); try { const headers = {}; if (req.headers.range) headers.Range = req.headers.range; headers['User-Agent'] = req.headers['user-agent'] || 'Mozilla/5.0'; headers['Referer'] = 'https://www.youtube.com/'; const upstream = await axios.get(target, { responseType: 'stream', headers, validateStatus: () => true, maxRedirects: 5 }); res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Credentials', 'true'); res.setHeader('Access-Control-Expose-Headers', 'Content-Range, Accept-Ranges'); res.status(upstream.status); ['content-type','content-length','content-range','accept-ranges'] .forEach(h => { if (upstream.headers[h]) res.setHeader(h, upstream.headers[h]); }); upstream.data.pipe(res); } catch { res.status(502).send('Proxy fetch failed'); } }); // Vidfly‑only YouTube resolver app.get('/api/yt/source', async (req, res) => { try { const raw = (req.query.url || '').trim(); if (!raw) return res.status(400).json({ error: 'Missing url' }); let watchUrl = raw; if (/^[A-Za-z0-9_-]{11}$/.test(raw)) { watchUrl = `https://www.youtube.com/watch?v=${raw}`; } const cacheKey = `vidfly:${watchUrl}`; let info = vidflyCache.get(cacheKey); if (!info) { const resp = await axios.get('https://api.vidfly.ai/api/media/youtube/download', { params: { url: watchUrl }, timeout: 20000 }); info = resp.data; vidflyCache.set(cacheKey, info); } if (info?.code !== 0 || !info?.data?.items?.length) { return res.status(502).json({ error: 'Vidfly: No playable stream' }); } const data = info.data; const items = Array.isArray(data.items) ? data.items : []; const muxed = items .filter(i => i?.type === 'video_with_audio' && i?.ext === 'mp4' && i?.url) .sort((a, b) => (b.height || 0) - (a.height || 0))[0]; const bestAudio = items .filter(i => i?.type === 'audio' && i?.url) .sort((a, b) => { const ba = parseInt(String(a.bitrate || '').replace(/\D/g, '') || '0', 10); const bb = parseInt(String(b.bitrate || '').replace(/\D/g, '') || '0', 10); return bb - ba; })[0]; const chosen = muxed || bestAudio; if (!chosen?.url) { return res.status(502).json({ error: 'Vidfly: No playable stream url' }); } res.json({ url: chosen.url, title: data.title || watchUrl, thumbnail: data.cover || null, duration: data.duration || null, kind: muxed ? 'video' : 'audio', source: 'vidfly' }); } catch (e) { res.status(500).json({ error: e.message || 'Vidfly resolve failed' }); } }); // YouTube v3 search app.get('/api/ytsearch', async (req, res) => { try { const key = process.env.YT_API_KEY; if (!key) return res.status(501).json({ error: 'YouTube search unavailable: set YT_API_KEY' }); const q = String(req.query.q || '').trim(); if (!q) return res.json({ items: [] }); const resp = await axios.get('https://www.googleapis.com/youtube/v3/search', { params: { key, q, part: 'snippet', type: 'video', maxResults: 12 }, timeout: 10000 }); const items = (resp.data.items || []) .map(it => ({ videoId: it.id?.videoId, title: it.snippet?.title, channelTitle: it.snippet?.channelTitle, thumb: it.snippet?.thumbnails?.medium?.url || it.snippet?.thumbnails?.default?.url })) .filter(x => x.videoId); res.json({ items }); } catch (e) { res.status(500).json({ error: e.message || 'YouTube search failed' }); } }); // JioSaavn endpoints app.get('/api/result', async (req, res) => { try { res.json(await searchUniversal(req.query.q || '')); } catch (e) { res.status(500).json({ error: e.message }); } }); app.get('/api/song', async (req, res) => { try { res.json(await getSong(req.query.q || '')); } catch (e) { res.status(500).json({ error: e.message }); } }); app.get('/api/album', async (req, res) => { try { res.json(await getAlbum(req.query.q || '')); } catch (e) { res.status(500).json({ error: e.message }); } }); app.get('/api/playlist', async (req, res) => { try { res.json(await getPlaylist(req.query.q || '')); } catch (e) { res.status(500).json({ error: e.message }); } }); app.get('/api/lyrics', async (req, res) => { try { res.json(await getLyrics(req.query.q || '')); } catch (e) { res.status(500).json({ error: e.message }); } }); // Lobby + Socket.IO sync const rooms = new Map(); function ensureRoom(id) { if (!rooms.has(id)) { rooms.set(id, { id, name: null, hostId: null, users: new Map(), track: null, isPlaying: false, anchor: 0, anchorAt: 0, queue: [] }); } return rooms.get(id); } function currentState(r) { return { track: r.track, isPlaying: r.isPlaying, anchor: r.anchor, anchorAt: r.anchorAt, queue: r.queue }; } function membersPayload(r) { return [...r.users.entries()].map(([id, u]) => ({ id, name: u.name, role: u.role, muted: !!u.muted, isHost: id === r.hostId })); } function broadcastMembers(id) { const r=rooms.get(id); if(r) io.to(id).emit('members',{members:membersPayload(r),roomName:r.name||r.id}); } function requireHostOrCohost(r,sid){const u=r.users.get(sid);return sid===r.hostId||(u&&u.role==='cohost');} function normalizeThumb(meta){return meta?.thumb||meta?.image||null;} function findUserByName(r, n) { if (!n) return null; const t = n.replace(/^@/, '').toLowerCase(); for (const [id, u] of r.users) { if ((u.name || '').toLowerCase() === t) return { id, u }; } return null; } app.get('/api/rooms', (_req, res) => { const data = [...rooms.values()] .filter(r => r.users.size > 0) .map(r => ({ id: r.id, name: r.name || r.id, members: r.users.size, isPlaying: r.isPlaying })); res.json({ rooms: data }); }); app.get('/api/ping', (_req, res) => res.json({ ok: true })); const clientDir = path.resolve(__dirname, '../client/dist'); app.use(express.static(clientDir)); app.get('*', (_req, res) => res.sendFile(path.join(clientDir, 'index.html'))); const server = http.createServer(app); const io = new SocketIOServer(server, { cors: { origin: '*', methods: ['GET', 'POST'] } }); io.on('connection', (socket) => { let joinedRoom = null; socket.on('join_room', ({ roomId, name, asHost = false, roomName = null }, ack) => { const room = ensureRoom(roomId); if (asHost || !room.hostId) room.hostId = socket.id; if (!room.name && roomName) room.name = String(roomName).trim().slice(0, 60) || null; const role = socket.id === room.hostId ? 'host' : 'member'; const cleanName = String(name || 'Guest').slice(0, 40); room.users.set(socket.id, { name: cleanName, role }); socket.join(roomId); joinedRoom = roomId; ack?.({ roomId, isHost: socket.id === room.hostId, state: currentState(room), roomName: room.name || room.id }); io.to(roomId).emit('system', { text: `System: ${cleanName} has joined the chat` }); broadcastMembers(roomId); }); socket.on('rename', ({ roomId, newName }) => { const room = rooms.get(roomId); if (!room) return; const u = room.users.get(socket.id); if (!u) return; u.name = String(newName || 'Guest').slice(0, 40); broadcastMembers(roomId); }); socket.on('chat_message', ({ roomId, name, text }) => { const room = rooms.get(roomId); if (!room) return; const u = room.users.get(socket.id); if (!u) return; if (u.muted) { socket.emit('system', { text: 'System: You are muted by the host' }); return; } socket.to(roomId).emit('chat_message', { name, text, at: Date.now() }); }); socket.on('song_request', ({ roomId, requester, query }) => { const room = rooms.get(roomId); if (!room) return; const payload = { requester, query, at: Date.now(), requestId: `${Date.now()}_${Math.random().toString(36).slice(2)}` }; for (const [id, usr] of room.users.entries()) { if (id === room.hostId || usr.role === 'cohost') io.to(id).emit('song_request', payload); } io.to(roomId).emit('system', { text: `System: ${requester} requested /play ${query}` }); }); socket.on('song_request_action', ({ roomId, action, track }) => { const room = rooms.get(roomId); if (!room) return; if (!requireHostOrCohost(room, socket.id)) return; if ((action === 'accept' || action === 'queue') && track) { const t = { url: track.url, title: track.title || track.url, meta: track.meta || {}, kind: track.kind || 'audio', thumb: track.thumb || normalizeThumb(track.meta || {}) }; if (action === 'accept') { room.track = t; room.isPlaying = false; room.anchor = 0; room.anchorAt = Date.now(); io.to(roomId).emit('set_track', { track: room.track }); io.to(roomId).emit('pause', { anchor: 0, anchorAt: room.anchorAt }); io.to(roomId).emit('system', { text: `System: Now playing ${room.track.title}` }); } else { room.queue.push(t); io.to(roomId).emit('queue_update', { queue: room.queue }); io.to(roomId).emit('system', { text: `System: Queued ${t.title}` }); } } }); socket.on('set_track', ({ roomId, track }) => { const room = rooms.get(roomId); if (!room || !requireHostOrCohost(room, socket.id)) return; const thumb = track.thumb || normalizeThumb(track.meta || {}); room.track = { ...track, thumb }; room.isPlaying = false; room.anchor = 0; room.anchorAt = Date.now(); io.to(roomId).emit('set_track', { track: room.track }); io.to(roomId).emit('pause', { anchor: 0, anchorAt: room.anchorAt }); io.to(roomId).emit('system', { text: `System: Selected ${room.track.title || room.track.url}` }); }); socket.on('play', ({ roomId }) => { const room = rooms.get(roomId); if (!room || !requireHostOrCohost(room, socket.id)) return; room.isPlaying = true; room.anchorAt = Date.now(); io.to(roomId).emit('play', { anchor: room.anchor, anchorAt: room.anchorAt }); }); socket.on('pause', ({ roomId }) => { const room = rooms.get(roomId); if (!room || !requireHostOrCohost(room, socket.id)) return; const elapsed = Math.max(0, Date.now() - room.anchorAt) / 1000; if (room.isPlaying) room.anchor += elapsed; room.isPlaying = false; room.anchorAt = Date.now(); io.to(roomId).emit('pause', { anchor: room.anchor, anchorAt: room.anchorAt }); }); socket.on('seek', ({ roomId, to }) => { const room = rooms.get(roomId); if (!room || !requireHostOrCohost(room, socket.id)) return; room.anchor = to; room.anchorAt = Date.now(); io.to(roomId).emit('seek', { anchor: room.anchor, anchorAt: room.anchorAt, isPlaying: room.isPlaying }); }); socket.on('ended', ({ roomId }) => { const room = rooms.get(roomId); if (!room || !requireHostOrCohost(room, socket.id)) return; const next = room.queue.shift(); if (next) { const thumb = next.thumb || normalizeThumb(next.meta || {}); room.track = { ...next, thumb }; room.isPlaying = false; room.anchor = 0; room.anchorAt = Date.now(); io.to(roomId).emit('set_track', { track: room.track }); io.to(roomId).emit('pause', { anchor: 0, anchorAt: room.anchorAt }); io.to(roomId).emit('queue_update', { queue: room.queue }); io.to(roomId).emit('system', { text: `System: Now playing ${room.track.title}` }); } else { io.to(roomId).emit('system', { text: 'System: Queue ended' }); } }); socket.on('admin_command', ({ roomId, cmd, targetName }) => { const room = rooms.get(roomId); const actor = room?.users.get(socket.id); if (!actor || !(socket.id === room.hostId || actor.role === 'cohost')) return; const found = findUserByName(room, targetName); if (!found) { return io.to(socket.id).emit('system', { text: `System: @${targetName} not found` }); } const { id, u } = found; if (cmd === 'kick') { io.to(id).emit('system', { text: 'System: You were kicked' }); io.sockets.sockets.get(id)?.leave(roomId); room.users.delete(id); io.to(roomId).emit('system', { text: `System: ${u.name} was kicked` }); broadcastMembers(roomId); } else if (cmd === 'promote') { u.role = 'cohost'; io.to(roomId).emit('system', { text: `System: ${u.name} promoted to co-host` }); broadcastMembers(roomId); } else if (cmd === 'mute') { u.muted = true; io.to(roomId).emit('system', { text: `System: ${u.name} was muted` }); broadcastMembers(roomId); } }); socket.on('sync_request', ({ roomId }, ack) => { const room = rooms.get(roomId); if (room) ack(currentState(room)); }); socket.on('disconnect', () => { if (!joinedRoom) return; const room = rooms.get(joinedRoom); if (!room) return; const leftUser = room.users.get(socket.id); room.users.delete(socket.id); if (socket.id === room.hostId) { const nextHost = [...room.users.keys()][0] || null; room.hostId = nextHost; if (nextHost) { const u = room.users.get(nextHost); if (u) u.role = 'host'; } io.to(joinedRoom).emit('host_changed', { hostId: room.hostId }); } if (leftUser) { const nm = leftUser.name || 'User'; io.to(joinedRoom).emit('system', { text: `System: ${nm} has left the chat` }); } if (room.users.size === 0) { rooms.delete(joinedRoom); } else { broadcastMembers(joinedRoom); } }); }); const PORT = process.env.PORT || 3000; server.listen(PORT, () => { console.log(`Server running on port ${PORT}`); });