// client/src/Room.jsx
import React, { useEffect, useMemo, useState } from 'react';
import { io } from 'socket.io-client';
import Player from './Player.jsx';
import Chat from './Chat.jsx';
import MemberList from './MemberList.jsx';
import { detectMediaTypeFromUrl, getThumb } from './utils.js';
import { useToasts } from './Toasts.jsx';
const socket = io('', { transports: ['websocket'] });
function DirectLinkModal({ open, onClose, onPick }) {
const [url, setUrl] = useState('');
const [type, setType] = useState('auto');
useEffect(() => { if (!open) { setUrl(''); setType('auto'); } }, [open]);
if (!open) return null;
return (
e.target === e.currentTarget && onClose()}>
Play a direct link
Paste a direct media URL (MP4, WEBM, MP3, M4A) or a YouTube URL/ID.
setUrl(e.target.value)}
placeholder="https://example.com/video.mp4 or https://youtu.be/xxxx or 11-char ID" />
);
}
function YouTubeModal({ open, onClose, onPick, ytError }) {
const [idOrUrl, setIdOrUrl] = useState('');
useEffect(() => { if (!open) setIdOrUrl(''); }, [open]);
if (!open) return null;
return (
e.target === e.currentTarget && onClose()}>
Play YouTube (Vidfly)
Enter a YouTube URL or 11‑character ID. Playback auto-switches to proxy if CORS blocks it.
setIdOrUrl(e.target.value)}
placeholder="https://www.youtube.com/watch?v=XXXXXXXXXXX or XXXXXXXID" />
{ytError && (
YouTube failed
{ytError.message}
)}
);
}
function RequestsPanel({ isHost, requests, onAct }) {
if (!isHost || !requests.length) return null;
return (
Song requests
{requests.map((r) => (
{r.requester} → {r.query}
{r.preview?.title || (r.preview ? 'Result found' : 'Searching…')}
))}
);
}
export default function Room({ roomId, name, asHost, roomName }) {
const { push } = useToasts();
const [isHost, setIsHost] = useState(false);
const [state, setState] = useState({ track: null, isPlaying: false, anchor: 0, anchorAt: 0, queue: [] });
const [members, setMembers] = useState([]);
const [partyName, setPartyName] = useState(roomName || roomId);
const [showDirect, setShowDirect] = useState(false);
const [showYT, setShowYT] = useState(false);
const [ytError, setYtError] = useState(null);
const [requests, setRequests] = useState([]);
useEffect(() => {
socket.emit('join_room', { roomId, name, asHost, roomName }, (resp) => {
setIsHost(resp.isHost);
if (resp.state) setState(s => ({ ...s, ...resp.state }));
if (resp.roomName) setPartyName(resp.roomName);
});
socket.on('set_track', ({ track }) => setState(s => ({ ...s, track })));
socket.on('play', ({ anchor, anchorAt }) => setState(s => ({ ...s, isPlaying: true, anchor, anchorAt })));
socket.on('pause', ({ anchor, anchorAt }) => setState(s => ({ ...s, isPlaying: false, anchor, anchorAt })));
socket.on('seek', ({ anchor, anchorAt, isPlaying }) => setState(s => ({ ...s, anchor, anchorAt, isPlaying })));
socket.on('host_changed', ({ hostId }) => setIsHost(socket.id === hostId));
socket.on('members', ({ members, roomName }) => { setMembers(members || []); if (roomName) setPartyName(roomName); });
socket.on('system', ({ text }) => { if (text) console.log(text); });
socket.on('queue_update', ({ queue }) => setState(s => ({ ...s, queue: queue || [] })));
// Host receives a request → attach a preview via JioSaavn search (best effort)
socket.on('song_request', async (req) => {
if (!isHost) return;
const enriched = { ...req, preview: null };
try {
const res = await fetch(`/api/result?q=${encodeURIComponent(req.query)}`);
const data = await res.json();
const list = Array.isArray(data?.data) ? data.data : (data?.results || data?.songs || data?.list || data?.items || data || []);
const first = list?.[0];
if (first) {
const url = first.media_url || first.downloadUrl || first.url || first.perma_url || first.streamUrl;
if (url) enriched.preview = { url, title: first.title || req.query, meta: first, kind: detectMediaTypeFromUrl(url) || 'audio', thumb: getThumb(first) };
}
} catch {}
setRequests(prev => [enriched, ...prev].slice(0, 20));
});
socket.emit('rename', { roomId, newName: name });
return () => {
socket.off('set_track'); socket.off('play'); socket.off('pause'); socket.off('seek');
socket.off('host_changed'); socket.off('members'); socket.off('system'); socket.off('queue_update'); socket.off('song_request');
};
}, [roomId, name, asHost, roomName, isHost]);
// Direct links
const setTrackAndClose = (track) => {
socket.emit('set_track', { roomId, track });
setShowDirect(false);
setShowYT(false);
};
const pickDirectUrl = async ({ url, type }) => {
if (!url) return;
const isYT = (type === 'youtube') || detectMediaTypeFromUrl(url) === 'youtube' || /^[A-Za-z0-9_-]{11}$/.test(url);
if (isYT) {
setShowDirect(false);
setShowYT(true);
return;
}
const mediaType = (type === 'auto' || !type) ? (detectMediaTypeFromUrl(url) || 'audio') : type;
if (mediaType === 'unknown') {
alert('Unknown media type. Choose Audio or Video.');
return;
}
const track = { url, title: url, meta: { source: 'direct' }, kind: mediaType };
setTrackAndClose(track);
};
// YouTube via Vidfly only
const resolveYouTube = async (idOrUrl) => {
setYtError(null);
const resp = await fetch(`/api/yt/source?url=${encodeURIComponent(idOrUrl)}`);
const data = await resp.json();
if (!resp.ok) {
const message = data?.error || 'Failed to resolve YouTube';
throw new Error(message);
}
return data;
};
const onPickYouTube = async (idOrUrl) => {
try {
const data = await resolveYouTube(idOrUrl);
const track = {
url: data.url,
title: data.title || idOrUrl,
meta: { thumb: data.thumbnail, source: data.source || 'vidfly', yt: true },
kind: data.kind || 'video',
thumb: data.thumbnail
};
setTrackAndClose(track);
} catch (e) {
setYtError({ message: e.message });
}
};
const Controls = useMemo(() => (
isHost ? (
) : Waiting for host controls…
), [isHost, roomId]);
const handleRequestAction = (action, req) => {
if (action === 'reject') {
setRequests(prev => prev.filter(r => r.requestId !== req.requestId));
return;
}
if (!req.preview) return;
const track = req.preview;
socket.emit('song_request_action', { roomId, action: action === 'accept' ? 'accept' : 'queue', track });
setRequests(prev => prev.filter(r => r.requestId !== req.requestId));
};
return (
{partyName}
{isHost ? 'Host' : 'Guest'}
{Controls}
Queue
{state.queue?.length ? (
{state.queue.map((t, i) => (
))}
) :
No songs in queue
}
setShowDirect(false)} onPick={pickDirectUrl} />
setShowYT(false)} onPick={onPickYouTube} ytError={ytError} />
);
}