akborana4 commited on
Commit
467de9c
·
verified ·
1 Parent(s): 1b7c80e

Update server/server.js

Browse files
Files changed (1) hide show
  1. server/server.js +86 -49
server/server.js CHANGED
@@ -35,17 +35,7 @@ function log(...args) {
35
  }
36
 
37
  // ---------- Room state ----------
38
- /*
39
- room = {
40
- id, name, hostId,
41
- users: Map<socketId, { name, role: 'host'|'cohost'|'member', muted?: boolean }>,
42
- track: { url, title, meta, kind, thumb? },
43
- isPlaying, anchor, anchorAt,
44
- queue: Array<track>
45
- }
46
- */
47
  const rooms = new Map();
48
-
49
  function ensureRoom(roomId) {
50
  if (!rooms.has(roomId)) {
51
  rooms.set(roomId, {
@@ -62,7 +52,6 @@ function ensureRoom(roomId) {
62
  }
63
  return rooms.get(roomId);
64
  }
65
-
66
  function currentState(room) {
67
  return {
68
  track: room.track,
@@ -72,7 +61,6 @@ function currentState(room) {
72
  queue: room.queue
73
  };
74
  }
75
-
76
  function membersPayload(room) {
77
  return [...room.users.entries()].map(([id, u]) => ({
78
  id,
@@ -82,20 +70,16 @@ function membersPayload(room) {
82
  isHost: id === room.hostId
83
  }));
84
  }
85
-
86
- // broadcastMembers uses `io` at runtime (ok because called after io is created)
87
  function broadcastMembers(roomId) {
88
  const room = rooms.get(roomId);
89
  if (!room) return;
90
  const members = membersPayload(room);
91
  io.to(roomId).emit('members', { members, roomName: room.name || room.id });
92
  }
93
-
94
  function requireHostOrCohost(room, socketId) {
95
  const u = room.users.get(socketId);
96
  return (socketId === room.hostId) || (u && (u.role === 'cohost'));
97
  }
98
-
99
  function normalizeThumb(meta) {
100
  const candidates = [
101
  meta?.thumb,
@@ -109,7 +93,6 @@ function normalizeThumb(meta) {
109
  ].filter(Boolean);
110
  return candidates[0] || null;
111
  }
112
-
113
  function findUserByName(room, targetNameRaw) {
114
  if (!targetNameRaw) return null;
115
  const targetName = String(targetNameRaw).replace(/^@/, '').trim().toLowerCase();
@@ -170,7 +153,7 @@ app.get('/api/lyrics', async (req, res) => {
170
  } catch (e) { log('/api/lyrics error', e?.message || short(e?.response?.data)); res.status(500).json({ error: e.message }); }
171
  });
172
 
173
- // ---------- YouTube search (YouTube Data API v3; optional) ----------
174
  app.get('/api/ytsearch', async (req, res) => {
175
  try {
176
  const key = process.env.YT_API_KEY;
@@ -194,58 +177,36 @@ app.get('/api/ytsearch', async (req, res) => {
194
  });
195
 
196
  // ---------- YouTube resolve via VidFly API ----------
197
- /*
198
- This route uses api.vidfly.ai -> expects response shape like:
199
- { code: 0, data: { title, cover, duration, items: [ { ext, fps, height, width, label, type, url } ] } }
200
- */
201
  app.get('/api/yt/source', async (req, res) => {
202
  try {
203
  let raw = (req.query.url || '').trim();
204
  if (!raw) return res.status(400).json({ error: 'Missing url' });
205
-
206
- // If input is just a video id, convert
207
- if (/^[a-zA-Z0-9_-]{11}$/.test(raw)) {
208
- raw = `https://www.youtube.com/watch?v=${raw}`;
209
- }
210
-
211
  const vidflyUrl = `https://api.vidfly.ai/api/media/youtube/download?url=${encodeURIComponent(raw)}`;
212
  log('Calling VidFly API:', vidflyUrl);
213
-
214
  const resp = await axios.get(vidflyUrl, { timeout: 20000, validateStatus: () => true });
215
  const data = resp.data;
216
-
217
  if (!data || typeof data !== 'object') {
218
  log('/api/yt/source vidfly non-object response:', short(data, 1000));
219
  return res.status(502).json({ error: 'Invalid VidFly response', snippet: short(data, 1000) });
220
  }
221
-
222
  if (Number(data.code) !== 0 || !data.data || !Array.isArray(data.data.items)) {
223
  log('/api/yt/source vidfly returned error shape:', short(data, 1000));
224
  return res.status(502).json({ error: 'Invalid VidFly API response', detail: data });
225
  }
226
-
227
  const items = data.data.items;
228
-
229
- // Prefer video_with_audio, pick highest resolution (height) available
230
  const progressive = items
231
  .filter(f => f.type === 'video_with_audio')
232
  .sort((a, b) => (Number(b.height || 0) - Number(a.height || 0)))[0];
233
-
234
- // Fallback: video-only highest resolution
235
  const videoOnly = items
236
  .filter(f => f.type === 'video')
237
  .sort((a, b) => (Number(b.height || 0) - Number(a.height || 0)))[0];
238
-
239
- // Fallback: any item with a URL
240
  const anyItem = items.find(f => f.url);
241
-
242
  const chosen = progressive || videoOnly || anyItem;
243
-
244
  if (!chosen || !chosen.url) {
245
  log('/api/yt/source no playable item found:', short(items, 1200));
246
  return res.status(502).json({ error: 'No playable format found in VidFly API response' });
247
  }
248
-
249
  res.json({
250
  url: chosen.url,
251
  title: data.data.title || raw,
@@ -265,6 +226,84 @@ app.get('/api/yt/source', async (req, res) => {
265
  }
266
  });
267
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
  // ---------- Lobby ----------
269
  app.get('/api/rooms', (_req, res) => {
270
  const data = [...rooms.values()]
@@ -295,24 +334,19 @@ io.on('connection', (socket) => {
295
 
296
  socket.on('join_room', ({ roomId, name, asHost = false, roomName = null }, ack) => {
297
  const room = ensureRoom(roomId);
298
-
299
  if (asHost || !room.hostId) room.hostId = socket.id;
300
  if (!room.name && roomName) room.name = String(roomName).trim().slice(0, 60) || null;
301
-
302
  const role = socket.id === room.hostId ? 'host' : 'member';
303
  const cleanName = String(name || 'Guest').slice(0, 40);
304
  room.users.set(socket.id, { name: cleanName, role });
305
-
306
  socket.join(roomId);
307
  joinedRoom = roomId;
308
-
309
  ack?.({
310
  roomId,
311
  isHost: socket.id === room.hostId,
312
  state: currentState(room),
313
  roomName: room.name || room.id
314
  });
315
-
316
  io.to(roomId).emit('system', { text: `System: ${cleanName} has joined the chat` });
317
  broadcastMembers(roomId);
318
  });
@@ -338,7 +372,11 @@ io.on('connection', (socket) => {
338
  socket.to(roomId).emit('chat_message', { name, text, at: Date.now() });
339
  });
340
 
341
- // /play request
 
 
 
 
342
  socket.on('song_request', ({ roomId, requester, query }) => {
343
  const room = rooms.get(roomId);
344
  if (!room) return;
@@ -440,13 +478,11 @@ io.on('connection', (socket) => {
440
  if (!room) return;
441
  const actor = room.users.get(socket.id);
442
  if (!actor || !(socket.id === room.hostId || actor.role === 'cohost')) return;
443
-
444
  const found = findUserByName(room, targetName);
445
  if (!found) {
446
  io.to(socket.id).emit('system', { text: `System: User @${targetName} not found` });
447
  return;
448
  }
449
-
450
  if (cmd === 'kick') {
451
  const { id } = found;
452
  io.to(id).emit('system', { text: 'System: You were kicked by the host' });
@@ -495,6 +531,7 @@ io.on('connection', (socket) => {
495
  });
496
  });
497
 
 
498
  const PORT = process.env.PORT || 3000;
499
  server.listen(PORT, () => {
500
  log(`Server running on port ${PORT}`);
 
35
  }
36
 
37
  // ---------- Room state ----------
 
 
 
 
 
 
 
 
 
38
  const rooms = new Map();
 
39
  function ensureRoom(roomId) {
40
  if (!rooms.has(roomId)) {
41
  rooms.set(roomId, {
 
52
  }
53
  return rooms.get(roomId);
54
  }
 
55
  function currentState(room) {
56
  return {
57
  track: room.track,
 
61
  queue: room.queue
62
  };
63
  }
 
64
  function membersPayload(room) {
65
  return [...room.users.entries()].map(([id, u]) => ({
66
  id,
 
70
  isHost: id === room.hostId
71
  }));
72
  }
 
 
73
  function broadcastMembers(roomId) {
74
  const room = rooms.get(roomId);
75
  if (!room) return;
76
  const members = membersPayload(room);
77
  io.to(roomId).emit('members', { members, roomName: room.name || room.id });
78
  }
 
79
  function requireHostOrCohost(room, socketId) {
80
  const u = room.users.get(socketId);
81
  return (socketId === room.hostId) || (u && (u.role === 'cohost'));
82
  }
 
83
  function normalizeThumb(meta) {
84
  const candidates = [
85
  meta?.thumb,
 
93
  ].filter(Boolean);
94
  return candidates[0] || null;
95
  }
 
96
  function findUserByName(room, targetNameRaw) {
97
  if (!targetNameRaw) return null;
98
  const targetName = String(targetNameRaw).replace(/^@/, '').trim().toLowerCase();
 
153
  } catch (e) { log('/api/lyrics error', e?.message || short(e?.response?.data)); res.status(500).json({ error: e.message }); }
154
  });
155
 
156
+ // ---------- YouTube search (optional) ----------
157
  app.get('/api/ytsearch', async (req, res) => {
158
  try {
159
  const key = process.env.YT_API_KEY;
 
177
  });
178
 
179
  // ---------- YouTube resolve via VidFly API ----------
 
 
 
 
180
  app.get('/api/yt/source', async (req, res) => {
181
  try {
182
  let raw = (req.query.url || '').trim();
183
  if (!raw) return res.status(400).json({ error: 'Missing url' });
184
+ if (/^[a-zA-Z0-9_-]{11}$/.test(raw)) raw = `https://www.youtube.com/watch?v=${raw}`;
 
 
 
 
 
185
  const vidflyUrl = `https://api.vidfly.ai/api/media/youtube/download?url=${encodeURIComponent(raw)}`;
186
  log('Calling VidFly API:', vidflyUrl);
 
187
  const resp = await axios.get(vidflyUrl, { timeout: 20000, validateStatus: () => true });
188
  const data = resp.data;
 
189
  if (!data || typeof data !== 'object') {
190
  log('/api/yt/source vidfly non-object response:', short(data, 1000));
191
  return res.status(502).json({ error: 'Invalid VidFly response', snippet: short(data, 1000) });
192
  }
 
193
  if (Number(data.code) !== 0 || !data.data || !Array.isArray(data.data.items)) {
194
  log('/api/yt/source vidfly returned error shape:', short(data, 1000));
195
  return res.status(502).json({ error: 'Invalid VidFly API response', detail: data });
196
  }
 
197
  const items = data.data.items;
 
 
198
  const progressive = items
199
  .filter(f => f.type === 'video_with_audio')
200
  .sort((a, b) => (Number(b.height || 0) - Number(a.height || 0)))[0];
 
 
201
  const videoOnly = items
202
  .filter(f => f.type === 'video')
203
  .sort((a, b) => (Number(b.height || 0) - Number(a.height || 0)))[0];
 
 
204
  const anyItem = items.find(f => f.url);
 
205
  const chosen = progressive || videoOnly || anyItem;
 
206
  if (!chosen || !chosen.url) {
207
  log('/api/yt/source no playable item found:', short(items, 1200));
208
  return res.status(502).json({ error: 'No playable format found in VidFly API response' });
209
  }
 
210
  res.json({
211
  url: chosen.url,
212
  title: data.data.title || raw,
 
226
  }
227
  });
228
 
229
+ /*
230
+ NEW: /api/yt/proxy
231
+ - Proxies the raw GoogleVideo (redirector) URL so browser won't hit CORS issues.
232
+ - Supports Range header (seeking).
233
+ - Validates host by default (only googlevideo.com).
234
+ - Usage: <video src="/api/yt/proxy?url=<ENCODED_REDIRECTOR_URL>">
235
+ */
236
+ function isAllowedProxyHost(urlStr) {
237
+ try {
238
+ const u = new URL(urlStr);
239
+ const host = u.hostname.toLowerCase();
240
+ // default allow only googlevideo redirector hosts; expand list if you use others
241
+ return host.endsWith('googlevideo.com') || host === 'redirector.googlevideo.com';
242
+ } catch {
243
+ return false;
244
+ }
245
+ }
246
+
247
+ app.get('/api/yt/proxy', async (req, res) => {
248
+ try {
249
+ const raw = (req.query.url || '').trim();
250
+ if (!raw) return res.status(400).json({ error: 'Missing url' });
251
+
252
+ if (!isAllowedProxyHost(raw)) {
253
+ // If you need to proxy other hosts, update isAllowedProxyHost accordingly.
254
+ return res.status(400).json({ error: 'Proxy to this host is not allowed by server policy' });
255
+ }
256
+
257
+ // Preserve Range header for partial content requests (seeking)
258
+ const rangeHeader = req.headers.range || null;
259
+ const headers = {};
260
+ if (rangeHeader) headers.Range = rangeHeader;
261
+
262
+ log('/api/yt/proxy fetching remote', short(raw, 200), 'Range:', rangeHeader ? rangeHeader.slice(0,120) : 'none');
263
+
264
+ // Use axios to stream remote response; allow non-2xx so we can forward status
265
+ const resp = await axios.get(raw, {
266
+ responseType: 'stream',
267
+ timeout: 20000,
268
+ headers,
269
+ validateStatus: () => true
270
+ });
271
+
272
+ // Forward key headers from remote to client (if present)
273
+ const hopByHop = new Set([
274
+ 'transfer-encoding', 'connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization',
275
+ 'te', 'trailer', 'upgrade'
276
+ ]);
277
+ const forwardable = ['content-type','content-length','content-range','accept-ranges','cache-control','last-modified','etag'];
278
+
279
+ forwardable.forEach(h => {
280
+ const v = resp.headers[h];
281
+ if (v !== undefined) res.setHeader(h, v);
282
+ });
283
+
284
+ // Ensure CORS header (app.use(cors()) covers most routes but for streamed responses set explicitly)
285
+ res.setHeader('Access-Control-Allow-Origin', '*');
286
+ // allow range requests from browsers
287
+ res.setHeader('Access-Control-Allow-Headers', 'Range,Accept,Content-Type');
288
+ // reflect remote status (206 if partial, 200 if full)
289
+ res.status(resp.status);
290
+
291
+ // Pipe the remote stream to the response
292
+ resp.data.pipe(res);
293
+
294
+ // error handling on the stream
295
+ resp.data.on('error', (err) => {
296
+ log('/api/yt/proxy remote stream error:', err?.message || String(err));
297
+ // if response not ended yet, try to send an error
298
+ try { if (!res.headersSent) res.status(502).json({ error: 'Remote stream error', detail: err?.message || String(err) }); } catch {}
299
+ });
300
+ } catch (e) {
301
+ log('/api/yt/proxy unexpected error:', e?.response?.status ?? e?.message ?? String(e));
302
+ const detail = e?.response?.data ?? e?.message ?? String(e);
303
+ res.status(500).json({ error: 'Failed to proxy remote video', detail: short(detail, 800) });
304
+ }
305
+ });
306
+
307
  // ---------- Lobby ----------
308
  app.get('/api/rooms', (_req, res) => {
309
  const data = [...rooms.values()]
 
334
 
335
  socket.on('join_room', ({ roomId, name, asHost = false, roomName = null }, ack) => {
336
  const room = ensureRoom(roomId);
 
337
  if (asHost || !room.hostId) room.hostId = socket.id;
338
  if (!room.name && roomName) room.name = String(roomName).trim().slice(0, 60) || null;
 
339
  const role = socket.id === room.hostId ? 'host' : 'member';
340
  const cleanName = String(name || 'Guest').slice(0, 40);
341
  room.users.set(socket.id, { name: cleanName, role });
 
342
  socket.join(roomId);
343
  joinedRoom = roomId;
 
344
  ack?.({
345
  roomId,
346
  isHost: socket.id === room.hostId,
347
  state: currentState(room),
348
  roomName: room.name || room.id
349
  });
 
350
  io.to(roomId).emit('system', { text: `System: ${cleanName} has joined the chat` });
351
  broadcastMembers(roomId);
352
  });
 
372
  socket.to(roomId).emit('chat_message', { name, text, at: Date.now() });
373
  });
374
 
375
+ // song_request, song_request_action, set_track, play/pause/seek/ended, admin_command, sync_request...
376
+ // (The rest of your socket handlers are unchanged — keep them exactly as before.)
377
+ // For brevity in this snippet, the same socket handlers from your previous file are used:
378
+ // copy/paste the rest of the socket handlers here (or keep this file from my previous response).
379
+ // ---------- START socket handlers ----------
380
  socket.on('song_request', ({ roomId, requester, query }) => {
381
  const room = rooms.get(roomId);
382
  if (!room) return;
 
478
  if (!room) return;
479
  const actor = room.users.get(socket.id);
480
  if (!actor || !(socket.id === room.hostId || actor.role === 'cohost')) return;
 
481
  const found = findUserByName(room, targetName);
482
  if (!found) {
483
  io.to(socket.id).emit('system', { text: `System: User @${targetName} not found` });
484
  return;
485
  }
 
486
  if (cmd === 'kick') {
487
  const { id } = found;
488
  io.to(id).emit('system', { text: 'System: You were kicked by the host' });
 
531
  });
532
  });
533
 
534
+ // start server
535
  const PORT = process.env.PORT || 3000;
536
  server.listen(PORT, () => {
537
  log(`Server running on port ${PORT}`);