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

Update server/server.js

Browse files
Files changed (1) hide show
  1. server/server.js +63 -102
server/server.js CHANGED
@@ -21,7 +21,7 @@ const app = express();
21
  app.use(cors());
22
  app.use(express.json());
23
 
24
- // --- Helpful small utils ---
25
  function short(v, n = 400) {
26
  try {
27
  const s = typeof v === 'string' ? v : JSON.stringify(v);
@@ -30,11 +30,22 @@ function short(v, n = 400) {
30
  return String(v).slice(0, n) + '…';
31
  }
32
  }
33
-
34
  function log(...args) {
35
  console.log(new Date().toISOString(), ...args);
36
  }
37
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  function ensureRoom(roomId) {
39
  if (!rooms.has(roomId)) {
40
  rooms.set(roomId, {
@@ -52,21 +63,6 @@ function ensureRoom(roomId) {
52
  return rooms.get(roomId);
53
  }
54
 
55
- // Health check
56
- app.get('/healthz', (_req, res) => res.send('OK'));
57
-
58
- // ---------- Room state ----------
59
- /*
60
- room = {
61
- id, name, hostId,
62
- users: Map<socketId, { name, role: 'host'|'cohost'|'member', muted?: boolean }>,
63
- track: { url, title, meta, kind, thumb? },
64
- isPlaying, anchor, anchorAt,
65
- queue: Array<track>
66
- }
67
- */
68
- const rooms = new Map();
69
-
70
  function currentState(room) {
71
  return {
72
  track: room.track,
@@ -87,6 +83,7 @@ function membersPayload(room) {
87
  }));
88
  }
89
 
 
90
  function broadcastMembers(roomId) {
91
  const room = rooms.get(roomId);
92
  if (!room) return;
@@ -130,6 +127,9 @@ function findUserByName(room, targetNameRaw) {
130
  return null;
131
  }
132
 
 
 
 
133
  // ---------- JioSaavn proxies ----------
134
  app.get('/api/result', async (req, res) => {
135
  try {
@@ -137,7 +137,7 @@ app.get('/api/result', async (req, res) => {
137
  const data = await searchUniversal(q);
138
  res.json(data);
139
  } catch (e) {
140
- log('/api/result error', e?.message || e);
141
  res.status(500).json({ error: e.message });
142
  }
143
  });
@@ -146,28 +146,28 @@ app.get('/api/song', async (req, res) => {
146
  const q = req.query.q || '';
147
  const data = await getSong(q);
148
  res.json(data);
149
- } catch (e) { log('/api/song error', e?.message || e); res.status(500).json({ error: e.message }); }
150
  });
151
  app.get('/api/album', async (req, res) => {
152
  try {
153
  const q = req.query.q || '';
154
  const data = await getAlbum(q);
155
  res.json(data);
156
- } catch (e) { log('/api/album error', e?.message || e); res.status(500).json({ error: e.message }); }
157
  });
158
  app.get('/api/playlist', async (req, res) => {
159
  try {
160
  const q = req.query.q || '';
161
  const data = await getPlaylist(q);
162
  res.json(data);
163
- } catch (e) { log('/api/playlist error', e?.message || e); res.status(500).json({ error: e.message }); }
164
  });
165
  app.get('/api/lyrics', async (req, res) => {
166
  try {
167
  const q = req.query.q || '';
168
  const data = await getLyrics(q);
169
  res.json(data);
170
- } catch (e) { log('/api/lyrics error', e?.message || e); res.status(500).json({ error: e.message }); }
171
  });
172
 
173
  // ---------- YouTube search (YouTube Data API v3; optional) ----------
@@ -193,108 +193,75 @@ app.get('/api/ytsearch', async (req, res) => {
193
  }
194
  });
195
 
196
- // ---------- YouTube resolve via your ytdl.php API ----------
197
  /*
198
- Env variables:
199
- - YTDL_API_KEY (required to call remote ytdl.php if you need to)
200
- - YTDL_API_BASE (optional) base host for ytdl.php, e.g. https://akborana.serv00.net
201
  */
202
  app.get('/api/yt/source', async (req, res) => {
203
  try {
204
- const key = process.env.YTDL_API_KEY;
205
- if (!key) {
206
- // Allow it to proceed if you want to use a ytdl endpoint that doesn't require a key;
207
- // but prefer to warn the user.
208
- log('Warning: YTDL_API_KEY not set. If your ytdl.php requires a key, requests will fail.');
209
- }
210
-
211
  let raw = (req.query.url || '').trim();
212
  if (!raw) return res.status(400).json({ error: 'Missing url' });
213
 
214
- // If it's just an 11-char YouTube id, convert to full watch URL:
215
  if (/^[a-zA-Z0-9_-]{11}$/.test(raw)) {
216
  raw = `https://www.youtube.com/watch?v=${raw}`;
217
  }
218
 
219
- const base = process.env.YTDL_API_BASE || 'https://akborana.serv00.net';
220
- const apiPath = process.env.YTDL_API_PATH || '/ytdl.php';
221
- const apiUrl = `${base.replace(/\/+$/, '')}${apiPath}?mode=formats&url=${encodeURIComponent(raw)}${key ? `&key=${encodeURIComponent(key)}` : ''}`;
222
 
223
- log('Calling ytdl.php at', apiUrl);
 
224
 
225
- const resp = await axios.get(apiUrl, { timeout: 20000, validateStatus: () => true });
 
 
 
226
 
227
- // normalize response
228
- let data = resp.data;
229
- // If we received HTML (common when HF space errors), show helpful snippet
230
- if (typeof data === 'string') {
231
- // try parse JSON if it is JSON-like
232
- try { data = JSON.parse(data); } catch (err) {
233
- log('/api/yt/source got non-JSON response snippet:', short(data, 800));
234
- return res.status(502).json({ error: 'ytdl.php returned non-JSON response', snippet: short(data, 800) });
235
- }
236
  }
237
 
238
- // Some ytdl wrappers return { formats: [...] } while others nest deeper.
239
- const formats = Array.isArray(data) ? data :
240
- Array.isArray(data.formats) ? data.formats :
241
- Array.isArray(data.result?.formats) ? data.result.formats :
242
- Array.isArray(data.data?.formats) ? data.data.formats : null;
243
 
244
- if (!formats || !Array.isArray(formats)) {
245
- log('/api/yt/source invalid formats from ytdl.php:', short(data, 800));
246
- return res.status(502).json({ error: 'Invalid formats response from ytdl.php', snippet: short(data, 800) });
247
- }
 
 
 
 
 
 
 
 
248
 
249
- // Find best progressive (video+audio) MP4/WebM preferably, fallback to best audio only
250
- const progressive = formats
251
- .filter(f => f.vcodec !== 'none' && f.acodec !== 'none')
252
- // prefer mp4 container (but don't filter out other containers)
253
- .sort((a, b) => {
254
- const szA = Number(a.filesize || a.filesizeBytes || 0);
255
- const szB = Number(b.filesize || b.filesizeBytes || 0);
256
- // prefer mp4 slightly
257
- const prefA = (String(a.ext || a.container || '').toLowerCase().includes('mp4')) ? 1 : 0;
258
- const prefB = (String(b.ext || b.container || '').toLowerCase().includes('mp4')) ? 1 : 0;
259
- return (prefB - prefA) || (szB - szA);
260
- })[0];
261
-
262
- const audioOnly = formats
263
- .filter(f => f.vcodec === 'none' && (String(f.ext || '').toLowerCase() === 'm4a' || String(f.ext || '').toLowerCase() === 'webm' || String(f.container || '').toLowerCase() === 'm4a'))
264
- .sort((a, b) => (Number(b.filesize || 0) - Number(a.filesize || 0)))[0];
265
-
266
- const chosen = progressive || audioOnly;
267
 
268
  if (!chosen || !chosen.url) {
269
- log('/api/yt/source no playable format; formats snippet:', short(formats, 1000));
270
- return res.status(502).json({ error: 'No playable format found in ytdl.php response', snippet: short(formats, 1000) });
271
  }
272
 
273
- const title = data.title || data.name || data.videoTitle || raw;
274
- const thumbnail = data.thumbnail || data.thumb || data.thumbnail_url || data.thumbnailUrl || null;
275
- const duration = data.duration || data.length || null;
276
- const kind = progressive ? 'video' : 'audio';
277
-
278
- // Return useful info back to client
279
  res.json({
280
  url: chosen.url,
281
- title,
282
- thumbnail,
283
- duration,
284
- kind,
285
- // small helpful debug info
286
  format: {
287
- ext: chosen.ext || chosen.container || null,
288
- vcodec: chosen.vcodec || null,
289
- acodec: chosen.acodec || null,
290
- filesize: chosen.filesize || chosen.filesizeBytes || null
291
  }
292
  });
293
  } catch (e) {
294
- // extract useful debug info if remote responded non-200
295
- const remoteData = e?.response?.data ?? e?.message ?? String(e);
296
- log('/api/yt/source error', short(remoteData, 1000));
297
- res.status(500).json({ error: 'Failed to fetch ytdl.php data', detail: short(remoteData, 1000) });
298
  }
299
  });
300
 
@@ -310,7 +277,6 @@ app.get('/api/rooms', (_req, res) => {
310
  }));
311
  res.json({ rooms: data });
312
  });
313
-
314
  app.get('/api/ping', (_req, res) => res.json({ ok: true }));
315
 
316
  // ---------- Static client ----------
@@ -347,7 +313,6 @@ io.on('connection', (socket) => {
347
  roomName: room.name || room.id
348
  });
349
 
350
- // Single clean system message to everyone
351
  io.to(roomId).emit('system', { text: `System: ${cleanName} has joined the chat` });
352
  broadcastMembers(roomId);
353
  });
@@ -384,7 +349,6 @@ io.on('connection', (socket) => {
384
  io.to(roomId).emit('system', { text: `System: ${requester} requested /play ${query}` });
385
  });
386
 
387
- // Host handles request action
388
  socket.on('song_request_action', ({ roomId, action, track }) => {
389
  const room = rooms.get(roomId);
390
  if (!room) return;
@@ -413,7 +377,6 @@ io.on('connection', (socket) => {
413
  }
414
  });
415
 
416
- // Host/cohost controls
417
  socket.on('set_track', ({ roomId, track }) => {
418
  const room = rooms.get(roomId);
419
  if (!room || !requireHostOrCohost(room, socket.id)) return;
@@ -453,7 +416,6 @@ io.on('connection', (socket) => {
453
  io.to(roomId).emit('seek', { anchor: room.anchor, anchorAt: room.anchorAt, isPlaying: room.isPlaying });
454
  });
455
 
456
- // Ended -> auto next
457
  socket.on('ended', ({ roomId }) => {
458
  const room = rooms.get(roomId);
459
  if (!room || !requireHostOrCohost(room, socket.id)) return;
@@ -473,7 +435,6 @@ io.on('connection', (socket) => {
473
  }
474
  });
475
 
476
- // Admin commands
477
  socket.on('admin_command', ({ roomId, cmd, targetName }) => {
478
  const room = rooms.get(roomId);
479
  if (!room) return;
 
21
  app.use(cors());
22
  app.use(express.json());
23
 
24
+ // --- small utils ---
25
  function short(v, n = 400) {
26
  try {
27
  const s = typeof v === 'string' ? v : JSON.stringify(v);
 
30
  return String(v).slice(0, n) + '…';
31
  }
32
  }
 
33
  function log(...args) {
34
  console.log(new Date().toISOString(), ...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, {
 
63
  return rooms.get(roomId);
64
  }
65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  function currentState(room) {
67
  return {
68
  track: room.track,
 
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;
 
127
  return null;
128
  }
129
 
130
+ // Health check
131
+ app.get('/healthz', (_req, res) => res.send('OK'));
132
+
133
  // ---------- JioSaavn proxies ----------
134
  app.get('/api/result', async (req, res) => {
135
  try {
 
137
  const data = await searchUniversal(q);
138
  res.json(data);
139
  } catch (e) {
140
+ log('/api/result error', e?.message || short(e?.response?.data));
141
  res.status(500).json({ error: e.message });
142
  }
143
  });
 
146
  const q = req.query.q || '';
147
  const data = await getSong(q);
148
  res.json(data);
149
+ } catch (e) { log('/api/song error', e?.message || short(e?.response?.data)); res.status(500).json({ error: e.message }); }
150
  });
151
  app.get('/api/album', async (req, res) => {
152
  try {
153
  const q = req.query.q || '';
154
  const data = await getAlbum(q);
155
  res.json(data);
156
+ } catch (e) { log('/api/album error', e?.message || short(e?.response?.data)); res.status(500).json({ error: e.message }); }
157
  });
158
  app.get('/api/playlist', async (req, res) => {
159
  try {
160
  const q = req.query.q || '';
161
  const data = await getPlaylist(q);
162
  res.json(data);
163
+ } catch (e) { log('/api/playlist error', e?.message || short(e?.response?.data)); res.status(500).json({ error: e.message }); }
164
  });
165
  app.get('/api/lyrics', async (req, res) => {
166
  try {
167
  const q = req.query.q || '';
168
  const data = await getLyrics(q);
169
  res.json(data);
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) ----------
 
193
  }
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,
252
+ thumbnail: data.data.cover || null,
253
+ duration: data.data.duration || null,
254
+ kind: chosen.type || 'video',
 
255
  format: {
256
+ ext: chosen.ext || null,
257
+ fps: chosen.fps || null,
258
+ resolution: `${chosen.width || '?'}x${chosen.height || '?'}`,
259
+ label: chosen.label || null
260
  }
261
  });
262
  } catch (e) {
263
+ log('/api/yt/source unexpected error:', e?.response?.data ?? e?.message ?? String(e));
264
+ res.status(500).json({ error: 'Failed to fetch from VidFly API', detail: e?.response?.data ?? e?.message ?? String(e) });
 
 
265
  }
266
  });
267
 
 
277
  }));
278
  res.json({ rooms: data });
279
  });
 
280
  app.get('/api/ping', (_req, res) => res.json({ ok: true }));
281
 
282
  // ---------- Static client ----------
 
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
  });
 
349
  io.to(roomId).emit('system', { text: `System: ${requester} requested /play ${query}` });
350
  });
351
 
 
352
  socket.on('song_request_action', ({ roomId, action, track }) => {
353
  const room = rooms.get(roomId);
354
  if (!room) return;
 
377
  }
378
  });
379
 
 
380
  socket.on('set_track', ({ roomId, track }) => {
381
  const room = rooms.get(roomId);
382
  if (!room || !requireHostOrCohost(room, socket.id)) return;
 
416
  io.to(roomId).emit('seek', { anchor: room.anchor, anchorAt: room.anchorAt, isPlaying: room.isPlaying });
417
  });
418
 
 
419
  socket.on('ended', ({ roomId }) => {
420
  const room = rooms.get(roomId);
421
  if (!room || !requireHostOrCohost(room, socket.id)) return;
 
435
  }
436
  });
437
 
 
438
  socket.on('admin_command', ({ roomId, cmd, targetName }) => {
439
  const room = rooms.get(roomId);
440
  if (!room) return;