akborana4 commited on
Commit
f0692cf
·
verified ·
1 Parent(s): e828353

Update server/server.js

Browse files
Files changed (1) hide show
  1. server/server.js +43 -34
server/server.js CHANGED
@@ -1,3 +1,7 @@
 
 
 
 
1
  import express from 'express';
2
  import http from 'http';
3
  import cors from 'cors';
@@ -19,7 +23,7 @@ const __dirname = path.dirname(__filename);
19
 
20
  const app = express();
21
 
22
- // CORS for API endpoints (the client is same origin; this also helps external embeds)
23
  app.use(cors({ origin: '*', credentials: true }));
24
  app.use(express.json());
25
 
@@ -30,35 +34,45 @@ const vidflyCache = new NodeCache({ stdTTL: 300, checkperiod: 60 });
30
  app.get('/healthz', (_req, res) => res.send('OK'));
31
 
32
  // ---------------- Media proxy (Range-aware, CORS-safe) ----------------
33
- // Use this for all Vidfly media URLs to bypass CORS.
 
 
 
 
 
 
 
 
 
34
  app.get('/api/proxy', async (req, res) => {
35
  const target = req.query.url;
36
  if (!target) return res.status(400).send('Missing url');
37
 
38
  try {
39
- // Forward Range and basic headers for streaming/seeking
40
  const headers = {};
41
  if (req.headers.range) headers.Range = req.headers.range;
42
- if (req.headers['user-agent']) headers['User-Agent'] = req.headers['user-agent'];
43
- if (req.headers['accept']) headers['Accept'] = req.headers['accept'];
44
  if (req.headers['accept-encoding']) headers['Accept-Encoding'] = req.headers['accept-encoding'];
45
  if (req.headers['accept-language']) headers['Accept-Language'] = req.headers['accept-language'];
46
- // Some CDNs check Referer; set a generic one if not present
47
- headers.Referer = req.headers.referer || 'https://www.youtube.com/';
48
 
49
  const upstream = await axios.get(target, {
50
  responseType: 'stream',
51
  headers,
52
- // We need to forward non-200 statuses for partial content
53
  validateStatus: () => true,
54
  maxRedirects: 5
55
  });
56
 
57
- // CORS headers for the proxied response (so <video> can consume it)
58
  res.setHeader('Access-Control-Allow-Origin', '*');
59
  res.setHeader('Access-Control-Allow-Credentials', 'true');
 
 
60
 
61
- // Mirror status and key headers (content-range enables seeking)
62
  res.status(upstream.status);
63
  const passthroughHeaders = [
64
  'content-type',
@@ -77,14 +91,17 @@ app.get('/api/proxy', async (req, res) => {
77
  if (v) res.setHeader(h, v);
78
  }
79
 
 
80
  upstream.data.pipe(res);
81
  } catch (e) {
 
82
  res.status(502).send('Proxy fetch failed');
83
  }
84
  });
85
 
86
- // ---------------- YouTube via Vidfly (no yt-dlp) ----------------
87
  // GET /api/yt/source?url=<watchURL or 11-char ID>
 
88
  app.get('/api/yt/source', async (req, res) => {
89
  try {
90
  const raw = (req.query.url || '').trim();
@@ -96,11 +113,10 @@ app.get('/api/yt/source', async (req, res) => {
96
  watchUrl = `https://www.youtube.com/watch?v=${raw}`;
97
  }
98
 
99
- // Cache
100
  const cacheKey = `vidfly:${watchUrl}`;
101
  let info = vidflyCache.get(cacheKey);
102
  if (!info) {
103
- // Vidfly API
104
  const resp = await axios.get('https://api.vidfly.ai/api/media/youtube/download', {
105
  params: { url: watchUrl },
106
  timeout: 20000
@@ -109,8 +125,8 @@ app.get('/api/yt/source', async (req, res) => {
109
  vidflyCache.set(cacheKey, info);
110
  }
111
 
112
- // Parse Vidfly shape you provided:
113
- // { code: 0, data: { cover, duration, title?, items: [ { ext, type, height, label, url, ... }, ... ] } }
114
  if (info?.code !== 0 || !info?.data?.items?.length) {
115
  return res.status(502).json({ error: 'Vidfly: No playable stream' });
116
  }
@@ -118,12 +134,12 @@ app.get('/api/yt/source', async (req, res) => {
118
  const data = info.data;
119
  const items = Array.isArray(data.items) ? data.items : [];
120
 
121
- // Prefer video_with_audio mp4 with highest height
122
  const muxed = items
123
  .filter(i => i?.type === 'video_with_audio' && i?.ext === 'mp4' && i?.url)
124
  .sort((a, b) => (b.height || 0) - (a.height || 0))[0];
125
 
126
- // Fallback: best "audio" (if provided)
127
  const bestAudio = items
128
  .filter(i => i?.type === 'audio' && i?.url)
129
  .sort((a, b) => {
@@ -137,7 +153,7 @@ app.get('/api/yt/source', async (req, res) => {
137
  return res.status(502).json({ error: 'Vidfly: No playable stream url' });
138
  }
139
 
140
- // Return the raw URL (client will route via /api/proxy automatically on error)
141
  return res.json({
142
  url: chosen.url,
143
  title: data.title || watchUrl,
@@ -152,7 +168,7 @@ app.get('/api/yt/source', async (req, res) => {
152
  });
153
 
154
  // ---------------- YouTube search (Data API v3) ----------------
155
- // Set env YT_API_KEY to enable
156
  app.get('/api/ytsearch', async (req, res) => {
157
  try {
158
  const key = process.env.YT_API_KEY;
@@ -267,12 +283,11 @@ function membersPayload(room) {
267
  function broadcastMembers(roomId) {
268
  const room = rooms.get(roomId);
269
  if (!room) return;
270
- const members = membersPayload(room);
271
- io.to(roomId).emit('members', { members, roomName: room.name || room.id });
272
  }
273
 
274
  function requireHostOrCohost(room, socketId) {
275
- const u = room.users.get(socket.id);
276
  return (socketId === room.hostId) || (u && u.role === 'cohost');
277
  }
278
 
@@ -381,8 +396,7 @@ io.on('connection', (socket) => {
381
  socket.on('song_request_action', ({ roomId, action, track }) => {
382
  const room = rooms.get(roomId);
383
  if (!room) return;
384
- const actor = room.users.get(socket.id);
385
- if (!actor || !(socket.id === room.hostId || actor.role === 'cohost')) return;
386
  if ((action === 'accept' || action === 'queue') && track) {
387
  const t = {
388
  url: track.url,
@@ -409,8 +423,7 @@ io.on('connection', (socket) => {
409
 
410
  socket.on('set_track', ({ roomId, track }) => {
411
  const room = rooms.get(roomId);
412
- const actor = room.users.get(socket.id);
413
- if (!room || !(socket.id === room.hostId || actor?.role === 'cohost')) return;
414
  const thumb = track.thumb || normalizeThumb(track.meta || {});
415
  room.track = { ...track, thumb };
416
  room.isPlaying = false;
@@ -423,8 +436,7 @@ io.on('connection', (socket) => {
423
 
424
  socket.on('play', ({ roomId }) => {
425
  const room = rooms.get(roomId);
426
- const actor = room.users.get(socket.id);
427
- if (!room || !(socket.id === room.hostId || actor?.role === 'cohost')) return;
428
  room.isPlaying = true;
429
  room.anchorAt = Date.now();
430
  io.to(roomId).emit('play', { anchor: room.anchor, anchorAt: room.anchorAt });
@@ -432,8 +444,7 @@ io.on('connection', (socket) => {
432
 
433
  socket.on('pause', ({ roomId }) => {
434
  const room = rooms.get(roomId);
435
- const actor = room.users.get(socket.id);
436
- if (!room || !(socket.id === room.hostId || actor?.role === 'cohost')) return;
437
  const elapsed = Math.max(0, Date.now() - room.anchorAt) / 1000;
438
  if (room.isPlaying) room.anchor += elapsed;
439
  room.isPlaying = false;
@@ -443,8 +454,7 @@ io.on('connection', (socket) => {
443
 
444
  socket.on('seek', ({ roomId, to }) => {
445
  const room = rooms.get(roomId);
446
- const actor = room.users.get(socket.id);
447
- if (!room || !(socket.id === room.hostId || actor?.role === 'cohost')) return;
448
  room.anchor = to;
449
  room.anchorAt = Date.now();
450
  io.to(roomId).emit('seek', { anchor: room.anchor, anchorAt: room.anchorAt, isPlaying: room.isPlaying });
@@ -452,8 +462,7 @@ io.on('connection', (socket) => {
452
 
453
  socket.on('ended', ({ roomId }) => {
454
  const room = rooms.get(roomId);
455
- const actor = room.users.get(socket.id);
456
- if (!room || !(socket.id === room.hostId || actor?.role === 'cohost')) return;
457
  const next = room.queue.shift();
458
  if (next) {
459
  const thumb = next.thumb || normalizeThumb(next.meta || {});
 
1
+ // server/server.js
2
+ // Complete server using Vidfly-only YouTube resolution + robust Range-aware proxy to bypass CORS,
3
+ // plus JioSaavn endpoints, YouTube v3 search, lobby, and full Socket.IO sync.
4
+
5
  import express from 'express';
6
  import http from 'http';
7
  import cors from 'cors';
 
23
 
24
  const app = express();
25
 
26
+ // Broad CORS for API routes (static assets are same-origin)
27
  app.use(cors({ origin: '*', credentials: true }));
28
  app.use(express.json());
29
 
 
34
  app.get('/healthz', (_req, res) => res.send('OK'));
35
 
36
  // ---------------- Media proxy (Range-aware, CORS-safe) ----------------
37
+ // Always route browser playback through this to bypass CORS on googlevideo.com.
38
+ // Supports Range for seeking, forwards useful headers, sets permissive CORS on response.
39
+ app.options('/api/proxy', (_req, res) => {
40
+ res.setHeader('Access-Control-Allow-Origin', '*');
41
+ res.setHeader('Access-Control-Allow-Headers', 'Range, Content-Type, Accept, Accept-Encoding, Accept-Language');
42
+ res.setHeader('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS');
43
+ res.setHeader('Access-Control-Allow-Credentials', 'true');
44
+ res.status(204).end();
45
+ });
46
+
47
  app.get('/api/proxy', async (req, res) => {
48
  const target = req.query.url;
49
  if (!target) return res.status(400).send('Missing url');
50
 
51
  try {
52
+ // Forward Range and some common headers that CDNs sometimes care about
53
  const headers = {};
54
  if (req.headers.range) headers.Range = req.headers.range;
55
+ headers['User-Agent'] = req.headers['user-agent'] || 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/116 Safari/537.36';
56
+ headers['Accept'] = req.headers['accept'] || '*/*';
57
  if (req.headers['accept-encoding']) headers['Accept-Encoding'] = req.headers['accept-encoding'];
58
  if (req.headers['accept-language']) headers['Accept-Language'] = req.headers['accept-language'];
59
+ headers['Referer'] = req.headers.referer || 'https://www.youtube.com/';
 
60
 
61
  const upstream = await axios.get(target, {
62
  responseType: 'stream',
63
  headers,
64
+ // Important: allow 206/416/etc to pass through
65
  validateStatus: () => true,
66
  maxRedirects: 5
67
  });
68
 
69
+ // Set permissive CORS on the proxied response
70
  res.setHeader('Access-Control-Allow-Origin', '*');
71
  res.setHeader('Access-Control-Allow-Credentials', 'true');
72
+ // Expose content-range so browsers can read seek info if needed
73
+ res.setHeader('Access-Control-Expose-Headers', 'Content-Range, Accept-Ranges');
74
 
75
+ // Mirror status and key headers necessary for media playback/seek
76
  res.status(upstream.status);
77
  const passthroughHeaders = [
78
  'content-type',
 
91
  if (v) res.setHeader(h, v);
92
  }
93
 
94
+ // Pipe body
95
  upstream.data.pipe(res);
96
  } catch (e) {
97
+ // Avoid leaking internal details
98
  res.status(502).send('Proxy fetch failed');
99
  }
100
  });
101
 
102
+ // ---------------- YouTube via Vidfly (yt-dlp removed) ----------------
103
  // GET /api/yt/source?url=<watchURL or 11-char ID>
104
+ // Returns direct stream URL from Vidfly; client will auto-route via /api/proxy if CORS blocks direct.
105
  app.get('/api/yt/source', async (req, res) => {
106
  try {
107
  const raw = (req.query.url || '').trim();
 
113
  watchUrl = `https://www.youtube.com/watch?v=${raw}`;
114
  }
115
 
116
+ // Cache Vidfly results
117
  const cacheKey = `vidfly:${watchUrl}`;
118
  let info = vidflyCache.get(cacheKey);
119
  if (!info) {
 
120
  const resp = await axios.get('https://api.vidfly.ai/api/media/youtube/download', {
121
  params: { url: watchUrl },
122
  timeout: 20000
 
125
  vidflyCache.set(cacheKey, info);
126
  }
127
 
128
+ // Expected shape:
129
+ // { code: 0, data: { title?, cover, duration, items: [ { ext, type, height, label, url, ... }, ... ] } }
130
  if (info?.code !== 0 || !info?.data?.items?.length) {
131
  return res.status(502).json({ error: 'Vidfly: No playable stream' });
132
  }
 
134
  const data = info.data;
135
  const items = Array.isArray(data.items) ? data.items : [];
136
 
137
+ // Prefer 'video_with_audio' mp4 with highest height
138
  const muxed = items
139
  .filter(i => i?.type === 'video_with_audio' && i?.ext === 'mp4' && i?.url)
140
  .sort((a, b) => (b.height || 0) - (a.height || 0))[0];
141
 
142
+ // Fallback: best audio (if provided)
143
  const bestAudio = items
144
  .filter(i => i?.type === 'audio' && i?.url)
145
  .sort((a, b) => {
 
153
  return res.status(502).json({ error: 'Vidfly: No playable stream url' });
154
  }
155
 
156
+ // Return raw URL; client will use direct src and auto-switch to /api/proxy on CORS error
157
  return res.json({
158
  url: chosen.url,
159
  title: data.title || watchUrl,
 
168
  });
169
 
170
  // ---------------- YouTube search (Data API v3) ----------------
171
+ // Enable by setting env YT_API_KEY
172
  app.get('/api/ytsearch', async (req, res) => {
173
  try {
174
  const key = process.env.YT_API_KEY;
 
283
  function broadcastMembers(roomId) {
284
  const room = rooms.get(roomId);
285
  if (!room) return;
286
+ io.to(roomId).emit('members', { members: membersPayload(room), roomName: room.name || room.id });
 
287
  }
288
 
289
  function requireHostOrCohost(room, socketId) {
290
+ const u = room.users.get(socketId);
291
  return (socketId === room.hostId) || (u && u.role === 'cohost');
292
  }
293
 
 
396
  socket.on('song_request_action', ({ roomId, action, track }) => {
397
  const room = rooms.get(roomId);
398
  if (!room) return;
399
+ if (!requireHostOrCohost(room, socket.id)) return;
 
400
  if ((action === 'accept' || action === 'queue') && track) {
401
  const t = {
402
  url: track.url,
 
423
 
424
  socket.on('set_track', ({ roomId, track }) => {
425
  const room = rooms.get(roomId);
426
+ if (!room || !requireHostOrCohost(room, socket.id)) return;
 
427
  const thumb = track.thumb || normalizeThumb(track.meta || {});
428
  room.track = { ...track, thumb };
429
  room.isPlaying = false;
 
436
 
437
  socket.on('play', ({ roomId }) => {
438
  const room = rooms.get(roomId);
439
+ if (!room || !requireHostOrCohost(room, socket.id)) return;
 
440
  room.isPlaying = true;
441
  room.anchorAt = Date.now();
442
  io.to(roomId).emit('play', { anchor: room.anchor, anchorAt: room.anchorAt });
 
444
 
445
  socket.on('pause', ({ roomId }) => {
446
  const room = rooms.get(roomId);
447
+ if (!room || !requireHostOrCohost(room, socket.id)) return;
 
448
  const elapsed = Math.max(0, Date.now() - room.anchorAt) / 1000;
449
  if (room.isPlaying) room.anchor += elapsed;
450
  room.isPlaying = false;
 
454
 
455
  socket.on('seek', ({ roomId, to }) => {
456
  const room = rooms.get(roomId);
457
+ if (!room || !requireHostOrCohost(room, socket.id)) return;
 
458
  room.anchor = to;
459
  room.anchorAt = Date.now();
460
  io.to(roomId).emit('seek', { anchor: room.anchor, anchorAt: room.anchorAt, isPlaying: room.isPlaying });
 
462
 
463
  socket.on('ended', ({ roomId }) => {
464
  const room = rooms.get(roomId);
465
+ if (!room || !requireHostOrCohost(room, socket.id)) return;
 
466
  const next = room.queue.shift();
467
  if (next) {
468
  const thumb = next.thumb || normalizeThumb(next.meta || {});