akborana4 commited on
Commit
df2fb16
·
verified ·
1 Parent(s): b485d97

Update server/server.js

Browse files
Files changed (1) hide show
  1. server/server.js +219 -37
server/server.js CHANGED
@@ -4,6 +4,7 @@ import cors from 'cors';
4
  import { Server as SocketIOServer } from 'socket.io';
5
  import path from 'path';
6
  import { fileURLToPath } from 'url';
 
7
  import {
8
  searchUniversal,
9
  getSong,
@@ -19,12 +20,17 @@ const app = express();
19
  app.use(cors());
20
  app.use(express.json());
21
 
22
- // Health check endpoint for Hugging Face
23
- app.get('/healthz', (req, res) => {
24
- res.send('OK');
25
- });
26
 
27
- // In-memory rooms
 
 
 
 
 
 
 
28
  const rooms = new Map();
29
 
30
  function ensureRoom(roomId) {
@@ -37,33 +43,60 @@ function ensureRoom(roomId) {
37
  track: null,
38
  isPlaying: false,
39
  anchor: 0,
40
- anchorAt: 0
 
41
  });
42
  }
43
  return rooms.get(roomId);
44
  }
45
 
46
- function broadcastMembers(roomId) {
47
- const room = rooms.get(roomId);
48
- if (!room) return;
49
- const members = [...room.users.entries()].map(([id, u]) => ({
 
 
 
 
 
 
 
 
50
  id,
51
  name: u.name || 'Guest',
 
 
52
  isHost: id === room.hostId
53
  }));
 
 
 
 
 
 
54
  io.to(roomId).emit('members', { members, roomName: room.name || room.id });
55
  }
56
 
57
- function currentState(room) {
58
- return {
59
- track: room.track,
60
- isPlaying: room.isPlaying,
61
- anchor: room.anchor,
62
- anchorAt: room.anchorAt
63
- };
64
  }
65
 
66
- // ---------- API endpoints ----------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  app.get('/api/result', async (req, res) => {
68
  try {
69
  const q = req.query.q || '';
@@ -114,8 +147,36 @@ app.get('/api/lyrics', async (req, res) => {
114
  }
115
  });
116
 
117
- // Active rooms list
118
- app.get('/api/rooms', (req, res) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  const data = [...rooms.values()]
120
  .filter(r => r.users.size > 0)
121
  .map(r => ({
@@ -127,14 +188,12 @@ app.get('/api/rooms', (req, res) => {
127
  res.json({ rooms: data });
128
  });
129
 
130
- app.get('/api/ping', (req, res) => res.json({ ok: true }));
131
 
132
- // Serve frontend build at root
133
  const clientDir = path.resolve(__dirname, '../client/dist');
134
  app.use(express.static(clientDir));
135
- app.get('*', (_, res) => {
136
- res.sendFile(path.join(clientDir, 'index.html'));
137
- });
138
 
139
  // ---------- Socket.IO ----------
140
  const server = http.createServer(app);
@@ -147,11 +206,13 @@ io.on('connection', (socket) => {
147
 
148
  socket.on('join_room', ({ roomId, name, asHost = false, roomName = null }, ack) => {
149
  const room = ensureRoom(roomId);
150
- room.users.set(socket.id, { name: name || 'Guest' });
151
 
152
  if (asHost || !room.hostId) room.hostId = socket.id;
153
  if (!room.name && roomName) room.name = String(roomName).trim().slice(0, 60) || null;
154
 
 
 
 
155
  socket.join(roomId);
156
  joinedRoom = roomId;
157
 
@@ -162,37 +223,93 @@ io.on('connection', (socket) => {
162
  roomName: room.name || room.id
163
  });
164
 
165
- socket.to(roomId).emit('system', { type: 'join', name, id: socket.id });
166
  broadcastMembers(roomId);
167
  });
168
 
169
  socket.on('rename', ({ roomId, newName }) => {
170
  const room = rooms.get(roomId);
171
  if (!room) return;
172
- if (room.users.has(socket.id)) {
173
- room.users.set(socket.id, { name: String(newName || 'Guest').slice(0, 40) });
174
- broadcastMembers(roomId);
175
- }
176
  });
177
 
178
  socket.on('chat_message', ({ roomId, name, text }) => {
 
 
 
 
 
 
 
 
179
  socket.to(roomId).emit('chat_message', { name, text, at: Date.now() });
180
  });
181
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  socket.on('set_track', ({ roomId, track }) => {
183
  const room = rooms.get(roomId);
184
- if (!room || socket.id !== room.hostId) return;
185
- room.track = track;
 
186
  room.isPlaying = false;
187
  room.anchor = 0;
188
  room.anchorAt = Date.now();
189
- io.to(roomId).emit('set_track', { track });
190
  io.to(roomId).emit('pause', { anchor: 0, anchorAt: room.anchorAt });
 
191
  });
192
 
193
  socket.on('play', ({ roomId }) => {
194
  const room = rooms.get(roomId);
195
- if (!room || socket.id !== room.hostId) return;
196
  room.isPlaying = true;
197
  room.anchorAt = Date.now();
198
  io.to(roomId).emit('play', { anchor: room.anchor, anchorAt: room.anchorAt });
@@ -200,7 +317,7 @@ io.on('connection', (socket) => {
200
 
201
  socket.on('pause', ({ roomId }) => {
202
  const room = rooms.get(roomId);
203
- if (!room || socket.id !== room.hostId) return;
204
  const elapsed = Math.max(0, Date.now() - room.anchorAt) / 1000;
205
  if (room.isPlaying) room.anchor += elapsed;
206
  room.isPlaying = false;
@@ -210,12 +327,69 @@ io.on('connection', (socket) => {
210
 
211
  socket.on('seek', ({ roomId, to }) => {
212
  const room = rooms.get(roomId);
213
- if (!room || socket.id !== room.hostId) return;
214
  room.anchor = to;
215
  room.anchorAt = Date.now();
216
  io.to(roomId).emit('seek', { anchor: room.anchor, anchorAt: room.anchorAt, isPlaying: room.isPlaying });
217
  });
218
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
  socket.on('sync_request', ({ roomId }, ack) => {
220
  const room = rooms.get(roomId);
221
  if (!room) return;
@@ -226,12 +400,20 @@ io.on('connection', (socket) => {
226
  if (!joinedRoom) return;
227
  const room = rooms.get(joinedRoom);
228
  if (!room) return;
 
229
  room.users.delete(socket.id);
230
  if (socket.id === room.hostId) {
231
  const nextHost = [...room.users.keys()][0];
232
  room.hostId = nextHost || null;
 
 
 
 
233
  io.to(joinedRoom).emit('host_changed', { hostId: room.hostId });
234
  }
 
 
 
235
  if (room.users.size === 0) {
236
  rooms.delete(joinedRoom);
237
  } else {
 
4
  import { Server as SocketIOServer } from 'socket.io';
5
  import path from 'path';
6
  import { fileURLToPath } from 'url';
7
+ import axios from 'axios';
8
  import {
9
  searchUniversal,
10
  getSong,
 
20
  app.use(cors());
21
  app.use(express.json());
22
 
23
+ // Health check endpoint
24
+ app.get('/healthz', (req, res) => res.send('OK'));
 
 
25
 
26
+ // Room model:
27
+ // {
28
+ // id, name, hostId,
29
+ // users: Map<socketId, { name, role: 'host'|'cohost'|'member', muted?: boolean }>
30
+ // track: { url, title, meta, kind, thumb? },
31
+ // isPlaying, anchor, anchorAt,
32
+ // queue: Array<track>
33
+ // }
34
  const rooms = new Map();
35
 
36
  function ensureRoom(roomId) {
 
43
  track: null,
44
  isPlaying: false,
45
  anchor: 0,
46
+ anchorAt: 0,
47
+ queue: []
48
  });
49
  }
50
  return rooms.get(roomId);
51
  }
52
 
53
+ function currentState(room) {
54
+ return {
55
+ track: room.track,
56
+ isPlaying: room.isPlaying,
57
+ anchor: room.anchor,
58
+ anchorAt: room.anchorAt,
59
+ queue: room.queue
60
+ };
61
+ }
62
+
63
+ function membersPayload(room) {
64
+ return [...room.users.entries()].map(([id, u]) => ({
65
  id,
66
  name: u.name || 'Guest',
67
+ role: u.role || 'member',
68
+ muted: !!u.muted,
69
  isHost: id === room.hostId
70
  }));
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
 
80
+ function requireHostOrCohost(room, socketId) {
81
+ const u = room.users.get(socketId);
82
+ return (socketId === room.hostId) || (u && (u.role === 'cohost'));
 
 
 
 
83
  }
84
 
85
+ function normalizeThumb(meta) {
86
+ const candidates = [
87
+ meta?.image,
88
+ meta?.thumbnail,
89
+ meta?.song_image,
90
+ meta?.album_image,
91
+ meta?.image_url,
92
+ meta?.images?.[0],
93
+ meta?.images?.medium,
94
+ meta?.images?.cover
95
+ ].filter(Boolean);
96
+ return candidates[0] || null;
97
+ }
98
+
99
+ // ---------- API endpoints (JioSaavn proxy) ----------
100
  app.get('/api/result', async (req, res) => {
101
  try {
102
  const q = req.query.q || '';
 
147
  }
148
  });
149
 
150
+ // ---------- YouTube search (requires YT_API_KEY env) ----------
151
+ app.get('/api/ytsearch', async (req, res) => {
152
+ try {
153
+ const key = process.env.YT_API_KEY;
154
+ if (!key) return res.status(501).json({ error: 'YouTube search unavailable: set YT_API_KEY env' });
155
+ const q = req.query.q || '';
156
+ const resp = await axios.get('https://www.googleapis.com/youtube/v3/search', {
157
+ params: {
158
+ key,
159
+ q,
160
+ part: 'snippet',
161
+ type: 'video',
162
+ maxResults: 10
163
+ },
164
+ timeout: 10000
165
+ });
166
+ const items = (resp.data.items || []).map(it => ({
167
+ videoId: it.id?.videoId,
168
+ title: it.snippet?.title,
169
+ channelTitle: it.snippet?.channelTitle,
170
+ thumb: it.snippet?.thumbnails?.medium?.url || it.snippet?.thumbnails?.default?.url
171
+ })).filter(x => x.videoId);
172
+ res.json({ items });
173
+ } catch (e) {
174
+ res.status(500).json({ error: e.message });
175
+ }
176
+ });
177
+
178
+ // Active rooms for lobby
179
+ app.get('/api/rooms', (_req, res) => {
180
  const data = [...rooms.values()]
181
  .filter(r => r.users.size > 0)
182
  .map(r => ({
 
188
  res.json({ rooms: data });
189
  });
190
 
191
+ app.get('/api/ping', (_req, res) => res.json({ ok: true }));
192
 
193
+ // Serve frontend build
194
  const clientDir = path.resolve(__dirname, '../client/dist');
195
  app.use(express.static(clientDir));
196
+ app.get('*', (_req, res) => res.sendFile(path.join(clientDir, 'index.html')));
 
 
197
 
198
  // ---------- Socket.IO ----------
199
  const server = http.createServer(app);
 
206
 
207
  socket.on('join_room', ({ roomId, name, asHost = false, roomName = null }, ack) => {
208
  const room = ensureRoom(roomId);
 
209
 
210
  if (asHost || !room.hostId) room.hostId = socket.id;
211
  if (!room.name && roomName) room.name = String(roomName).trim().slice(0, 60) || null;
212
 
213
+ const role = socket.id === room.hostId ? 'host' : 'member';
214
+ room.users.set(socket.id, { name: String(name || 'Guest').slice(0, 40), role });
215
+
216
  socket.join(roomId);
217
  joinedRoom = roomId;
218
 
 
223
  roomName: room.name || room.id
224
  });
225
 
226
+ io.to(roomId).emit('system', { text: `System: ${name || 'Guest'} has joined the chat` });
227
  broadcastMembers(roomId);
228
  });
229
 
230
  socket.on('rename', ({ roomId, newName }) => {
231
  const room = rooms.get(roomId);
232
  if (!room) return;
233
+ const u = room.users.get(socket.id);
234
+ if (!u) return;
235
+ u.name = String(newName || 'Guest').slice(0, 40);
236
+ broadcastMembers(roomId);
237
  });
238
 
239
  socket.on('chat_message', ({ roomId, name, text }) => {
240
+ const room = rooms.get(roomId);
241
+ if (!room) return;
242
+ const u = room.users.get(socket.id);
243
+ if (!u) return;
244
+ if (u.muted) {
245
+ socket.emit('system', { text: 'System: You are muted by the host' });
246
+ return;
247
+ }
248
  socket.to(roomId).emit('chat_message', { name, text, at: Date.now() });
249
  });
250
 
251
+ // User /play request -> notify host/cohosts and room
252
+ socket.on('song_request', ({ roomId, requester, query }) => {
253
+ const room = rooms.get(roomId);
254
+ if (!room) return;
255
+ const payload = { requester, query, at: Date.now(), requestId: `${Date.now()}_${Math.random().toString(36).slice(2)}` };
256
+ for (const [id, usr] of room.users.entries()) {
257
+ if (id === room.hostId || usr.role === 'cohost') {
258
+ io.to(id).emit('song_request', payload);
259
+ }
260
+ }
261
+ io.to(roomId).emit('system', { text: `System: ${requester} requested /play ${query}` });
262
+ });
263
+
264
+ // Host handles request action
265
+ socket.on('song_request_action', ({ roomId, action, track }) => {
266
+ const room = rooms.get(roomId);
267
+ if (!room) return;
268
+ if (!requireHostOrCohost(room, socket.id)) return;
269
+ if (action === 'accept' && track) {
270
+ room.track = {
271
+ url: track.url,
272
+ title: track.title || track.url,
273
+ meta: track.meta || {},
274
+ kind: track.kind || 'audio',
275
+ thumb: track.thumb || normalizeThumb(track.meta || {})
276
+ };
277
+ room.isPlaying = false;
278
+ room.anchor = 0;
279
+ room.anchorAt = Date.now();
280
+ io.to(roomId).emit('set_track', { track: room.track });
281
+ io.to(roomId).emit('pause', { anchor: 0, anchorAt: room.anchorAt });
282
+ io.to(roomId).emit('system', { text: `System: Now playing ${room.track.title}` });
283
+ } else if (action === 'queue' && track) {
284
+ room.queue.push({
285
+ url: track.url,
286
+ title: track.title || track.url,
287
+ meta: track.meta || {},
288
+ kind: track.kind || 'audio',
289
+ thumb: track.thumb || normalizeThumb(track.meta || {})
290
+ });
291
+ io.to(roomId).emit('queue_update', { queue: room.queue });
292
+ io.to(roomId).emit('system', { text: `System: Queued ${track.title || track.url}` });
293
+ }
294
+ });
295
+
296
+ // Host controls
297
  socket.on('set_track', ({ roomId, track }) => {
298
  const room = rooms.get(roomId);
299
+ if (!room || !requireHostOrCohost(room, socket.id)) return;
300
+ const thumb = track.thumb || normalizeThumb(track.meta || {});
301
+ room.track = { ...track, thumb };
302
  room.isPlaying = false;
303
  room.anchor = 0;
304
  room.anchorAt = Date.now();
305
+ io.to(roomId).emit('set_track', { track: room.track });
306
  io.to(roomId).emit('pause', { anchor: 0, anchorAt: room.anchorAt });
307
+ io.to(roomId).emit('system', { text: `System: Selected ${room.track.title || room.track.url}` });
308
  });
309
 
310
  socket.on('play', ({ roomId }) => {
311
  const room = rooms.get(roomId);
312
+ if (!room || !requireHostOrCohost(room, socket.id)) return;
313
  room.isPlaying = true;
314
  room.anchorAt = Date.now();
315
  io.to(roomId).emit('play', { anchor: room.anchor, anchorAt: room.anchorAt });
 
317
 
318
  socket.on('pause', ({ roomId }) => {
319
  const room = rooms.get(roomId);
320
+ if (!room || !requireHostOrCohost(room, socket.id)) return;
321
  const elapsed = Math.max(0, Date.now() - room.anchorAt) / 1000;
322
  if (room.isPlaying) room.anchor += elapsed;
323
  room.isPlaying = false;
 
327
 
328
  socket.on('seek', ({ roomId, to }) => {
329
  const room = rooms.get(roomId);
330
+ if (!room || !requireHostOrCohost(room, socket.id)) return;
331
  room.anchor = to;
332
  room.anchorAt = Date.now();
333
  io.to(roomId).emit('seek', { anchor: room.anchor, anchorAt: room.anchorAt, isPlaying: room.isPlaying });
334
  });
335
 
336
+ // Ended -> auto next from queue
337
+ socket.on('ended', ({ roomId }) => {
338
+ const room = rooms.get(roomId);
339
+ if (!room || !requireHostOrCohost(room, socket.id)) return;
340
+ const next = room.queue.shift();
341
+ if (next) {
342
+ const thumb = next.thumb || normalizeThumb(next.meta || {});
343
+ room.track = { ...next, thumb };
344
+ room.isPlaying = false;
345
+ room.anchor = 0;
346
+ room.anchorAt = Date.now();
347
+ io.to(roomId).emit('set_track', { track: room.track });
348
+ io.to(roomId).emit('pause', { anchor: 0, anchorAt: room.anchorAt });
349
+ io.to(roomId).emit('queue_update', { queue: room.queue });
350
+ io.to(roomId).emit('system', { text: `System: Now playing ${room.track.title || room.track.url}` });
351
+ } else {
352
+ io.to(roomId).emit('system', { text: 'System: Queue ended' });
353
+ }
354
+ });
355
+
356
+ // Admin commands
357
+ socket.on('admin_command', ({ roomId, cmd, targetName }) => {
358
+ const room = rooms.get(roomId);
359
+ if (!room) return;
360
+ const actor = room.users.get(socket.id);
361
+ if (!actor || !(socket.id === room.hostId || actor.role === 'cohost')) return;
362
+
363
+ let targetEntry = null;
364
+ for (const [id, u] of room.users.entries()) {
365
+ if (u.name && u.name.toLowerCase() === targetName.toLowerCase()) {
366
+ targetEntry = { id, u };
367
+ break;
368
+ }
369
+ }
370
+ if (!targetEntry) {
371
+ io.to(socket.id).emit('system', { text: `System: User @${targetName} not found` });
372
+ return;
373
+ }
374
+
375
+ if (cmd === 'kick') {
376
+ const { id } = targetEntry;
377
+ io.to(id).emit('system', { text: 'System: You were kicked by the host' });
378
+ io.sockets.sockets.get(id)?.leave(roomId);
379
+ room.users.delete(id);
380
+ io.to(roomId).emit('system', { text: `System: ${targetEntry.u.name} was kicked` });
381
+ broadcastMembers(roomId);
382
+ } else if (cmd === 'promote') {
383
+ targetEntry.u.role = 'cohost';
384
+ io.to(roomId).emit('system', { text: `System: ${targetEntry.u.name} was promoted to co-host` });
385
+ broadcastMembers(roomId);
386
+ } else if (cmd === 'mute') {
387
+ targetEntry.u.muted = true;
388
+ io.to(roomId).emit('system', { text: `System: ${targetEntry.u.name} was muted` });
389
+ broadcastMembers(roomId);
390
+ }
391
+ });
392
+
393
  socket.on('sync_request', ({ roomId }, ack) => {
394
  const room = rooms.get(roomId);
395
  if (!room) return;
 
400
  if (!joinedRoom) return;
401
  const room = rooms.get(joinedRoom);
402
  if (!room) return;
403
+ const leftUser = room.users.get(socket.id);
404
  room.users.delete(socket.id);
405
  if (socket.id === room.hostId) {
406
  const nextHost = [...room.users.keys()][0];
407
  room.hostId = nextHost || null;
408
+ if (room.hostId) {
409
+ const u = room.users.get(room.hostId);
410
+ if (u) u.role = 'host';
411
+ }
412
  io.to(joinedRoom).emit('host_changed', { hostId: room.hostId });
413
  }
414
+ if (leftUser) {
415
+ io.to(joinedRoom).emit('system', { text: `System: ${leftUser.name || 'User'} has left the chat` });
416
+ }
417
  if (room.users.size === 0) {
418
  rooms.delete(joinedRoom);
419
  } else {