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

Update server/server.js

Browse files
Files changed (1) hide show
  1. server/server.js +114 -39
server/server.js CHANGED
@@ -1,3 +1,4 @@
 
1
  import express from 'express';
2
  import http from 'http';
3
  import cors from 'cors';
@@ -20,20 +21,19 @@ const app = express();
20
  app.use(cors());
21
  app.use(express.json());
22
 
23
- // Health check
24
- app.get('/healthz', (req, res) => res.send('OK'));
 
 
 
 
 
 
 
25
 
26
- // ---------- Room state ----------
27
- /*
28
- room = {
29
- id, name, hostId,
30
- users: Map<socketId, { name, role: 'host'|'cohost'|'member', muted?: boolean }>,
31
- track: { url, title, meta, kind, thumb? },
32
- isPlaying, anchor, anchorAt,
33
- queue: Array<track>
34
  }
35
- */
36
- const rooms = new Map();
37
 
38
  function ensureRoom(roomId) {
39
  if (!rooms.has(roomId)) {
@@ -52,6 +52,21 @@ function ensureRoom(roomId) {
52
  return rooms.get(roomId);
53
  }
54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  function currentState(room) {
56
  return {
57
  track: room.track,
@@ -122,6 +137,7 @@ app.get('/api/result', async (req, res) => {
122
  const data = await searchUniversal(q);
123
  res.json(data);
124
  } catch (e) {
 
125
  res.status(500).json({ error: e.message });
126
  }
127
  });
@@ -130,28 +146,28 @@ app.get('/api/song', async (req, res) => {
130
  const q = req.query.q || '';
131
  const data = await getSong(q);
132
  res.json(data);
133
- } catch (e) { res.status(500).json({ error: e.message }); }
134
  });
135
  app.get('/api/album', async (req, res) => {
136
  try {
137
  const q = req.query.q || '';
138
  const data = await getAlbum(q);
139
  res.json(data);
140
- } catch (e) { res.status(500).json({ error: e.message }); }
141
  });
142
  app.get('/api/playlist', async (req, res) => {
143
  try {
144
  const q = req.query.q || '';
145
  const data = await getPlaylist(q);
146
  res.json(data);
147
- } catch (e) { res.status(500).json({ error: e.message }); }
148
  });
149
  app.get('/api/lyrics', async (req, res) => {
150
  try {
151
  const q = req.query.q || '';
152
  const data = await getLyrics(q);
153
  res.json(data);
154
- } catch (e) { res.status(500).json({ error: e.message }); }
155
  });
156
 
157
  // ---------- YouTube search (YouTube Data API v3; optional) ----------
@@ -172,57 +188,116 @@ app.get('/api/ytsearch', async (req, res) => {
172
  })).filter(x => x.videoId);
173
  res.json({ items });
174
  } catch (e) {
 
175
  res.status(500).json({ error: e.message });
176
  }
177
  });
178
 
179
  // ---------- YouTube resolve via your ytdl.php API ----------
180
- // Replace the old /api/yt/source in server/server.js with this:
 
 
 
 
181
  app.get('/api/yt/source', async (req, res) => {
182
  try {
183
  const key = process.env.YTDL_API_KEY;
184
- if (!key) return res.status(500).json({ error: 'Missing YTDL_API_KEY env var' });
 
 
 
 
185
 
186
  let raw = (req.query.url || '').trim();
187
  if (!raw) return res.status(400).json({ error: 'Missing url' });
188
 
189
- // If it's just an 11-char ID, make it a full watch URL
190
  if (/^[a-zA-Z0-9_-]{11}$/.test(raw)) {
191
  raw = `https://www.youtube.com/watch?v=${raw}`;
192
  }
193
 
194
- const apiUrl = `https://akborana.serv00.net/ytdl.php?mode=formats&url=${encodeURIComponent(raw)}&key=${encodeURIComponent(key)}`;
195
- const resp = await axios.get(apiUrl, { timeout: 15000 });
196
- const data = resp.data;
 
 
 
 
197
 
198
- if (!data || !Array.isArray(data.formats)) {
199
- return res.status(502).json({ error: 'Invalid formats response' });
 
 
 
 
 
 
 
200
  }
201
 
202
- const progressive = data.formats
203
- .filter(f => f.ext === 'mp4' && f.vcodec !== 'none' && f.acodec !== 'none')
204
- .sort((a, b) => (b.filesize || 0) - (a.filesize || 0))[0];
 
 
 
 
 
 
 
205
 
206
- const audioOnly = data.formats
207
- .filter(f => f.vcodec === 'none' && (f.ext === 'm4a' || f.ext === 'webm'))
208
- .sort((a, b) => (b.filesize || 0) - (a.filesize || 0))[0];
 
 
 
 
 
 
 
 
 
 
 
 
 
209
 
210
  const chosen = progressive || audioOnly;
211
- if (!chosen) return res.status(502).json({ error: 'No playable format found' });
212
 
 
 
 
 
 
 
 
 
 
 
 
213
  res.json({
214
  url: chosen.url,
215
- title: data.title || raw,
216
- thumbnail: data.thumbnail || null,
217
- duration: data.duration || null,
218
- kind: progressive ? 'video' : 'audio'
 
 
 
 
 
 
 
219
  });
220
  } catch (e) {
221
- res.status(500).json({ error: e.message });
 
 
 
222
  }
223
  });
224
 
225
-
226
  // ---------- Lobby ----------
227
  app.get('/api/rooms', (_req, res) => {
228
  const data = [...rooms.values()]
@@ -461,5 +536,5 @@ io.on('connection', (socket) => {
461
 
462
  const PORT = process.env.PORT || 3000;
463
  server.listen(PORT, () => {
464
- console.log(`Server running on port ${PORT}`);
465
- });
 
1
+ // server.js
2
  import express from 'express';
3
  import http from 'http';
4
  import cors from 'cors';
 
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);
28
+ return s.length > n ? s.slice(0, n) + '…' : s;
29
+ } catch {
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)) {
 
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,
 
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
  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) ----------
 
188
  })).filter(x => x.videoId);
189
  res.json({ items });
190
  } catch (e) {
191
+ log('/api/ytsearch error', e?.message || short(e?.response?.data));
192
  res.status(500).json({ error: e.message });
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
 
 
301
  // ---------- Lobby ----------
302
  app.get('/api/rooms', (_req, res) => {
303
  const data = [...rooms.values()]
 
536
 
537
  const PORT = process.env.PORT || 3000;
538
  server.listen(PORT, () => {
539
+ log(`Server running on port ${PORT}`);
540
+ });