test / server /server.js
akborana4's picture
Update server/server.js
83623a0 verified
// 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}`);
});