// MusicRoom - Stream Sync module // WebSocket connection and server synchronization (function() { const M = window.MusicRoom; // Load available streams and connect to first one M.loadStreams = async function() { try { const res = await fetch("/api/streams"); const streams = await res.json(); if (streams.length === 0) { M.$("#track-title").textContent = "No streams available"; return; } if (streams.length > 1) { const sel = document.createElement("select"); for (const s of streams) { const opt = document.createElement("option"); opt.value = s.id; opt.textContent = s.name; sel.appendChild(opt); } sel.onchange = () => M.connectStream(sel.value); M.$("#stream-select").appendChild(sel); } M.connectStream(streams[0].id); } catch (e) { M.$("#track-title").textContent = "Server unavailable"; M.$("#status").textContent = "Local (offline)"; M.synced = false; M.updateUI(); } }; // Connect to a stream via WebSocket M.connectStream = function(id) { if (M.ws) { const oldWs = M.ws; M.ws = null; oldWs.onclose = null; oldWs.close(); } M.currentStreamId = id; const proto = location.protocol === "https:" ? "wss:" : "ws:"; M.ws = new WebSocket(proto + "//" + location.host + "/api/streams/" + id + "/ws"); M.ws.onmessage = (e) => { const data = JSON.parse(e.data); // Handle library updates if (data.type === "track_added") { M.showToast(`"${data.track.title}" is now available`); if (data.library) { M.library = data.library; M.renderLibrary(); if (M.selectedPlaylistId === "all") { M.playlist = [...M.library]; M.renderPlaylist(); } } return; } if (data.type === "track_removed") { M.showToast(`"${data.track.title}" was removed`); if (data.library) { M.library = data.library; M.renderLibrary(); if (M.selectedPlaylistId === "all") { M.playlist = [...M.library]; M.renderPlaylist(); } } return; } // Normal stream update M.handleUpdate(data); }; M.ws.onclose = () => { M.synced = false; M.ws = null; M.$("#sync-indicator").classList.add("disconnected"); M.updateUI(); // Auto-reconnect if user wants to be synced if (M.wantSync) { setTimeout(() => M.connectStream(id), 3000); } }; M.ws.onopen = () => { M.synced = true; M.$("#sync-indicator").classList.remove("disconnected"); M.updateUI(); }; }; // Handle stream state update from server M.handleUpdate = async function(data) { if (!data.track) { M.$("#track-title").textContent = "No tracks"; return; } M.$("#stream-name").textContent = data.streamName || ""; M.serverTimestamp = data.currentTimestamp; M.serverTrackDuration = data.track.duration; M.lastServerUpdate = Date.now(); const wasServerPaused = M.serverPaused; M.serverPaused = data.paused ?? true; // Update playlist if provided if (data.playlist) { M.playlist = data.playlist; M.currentIndex = data.currentIndex ?? 0; M.renderPlaylist(); } else if (data.currentIndex !== undefined && data.currentIndex !== M.currentIndex) { M.currentIndex = data.currentIndex; M.renderPlaylist(); } // Cache track info for local mode - use track.id (content hash) as the identifier const trackId = data.track.id || data.track.filename; // Fallback for compatibility const isNewTrack = trackId !== M.currentTrackId; if (isNewTrack) { M.currentTrackId = trackId; M.currentTitle = data.track.title; M.$("#track-title").textContent = data.track.title; M.loadingSegments.clear(); // Debug: log cache state for this track const trackCache = M.trackCaches.get(trackId); console.log("[Playback] Starting track:", data.track.title, { trackId: trackId, segments: trackCache ? [...trackCache] : [], segmentCount: trackCache ? trackCache.size : 0, inCachedTracks: M.cachedTracks.has(trackId), bulkStarted: M.bulkDownloadStarted.get(trackId) || false, hasBlobUrl: M.trackBlobs.has(trackId) }); // Check if this track already has all segments cached M.checkAndCacheComplete(trackId); } if (M.synced) { if (!M.serverPaused) { // Server is playing - ensure we're playing and synced if (isNewTrack || !M.audio.src) { // Try cache first const cachedUrl = await M.loadTrackBlob(M.currentTrackId); M.audio.src = cachedUrl || M.getTrackUrl(M.currentTrackId); } if (M.audio.paused) { M.audio.currentTime = data.currentTimestamp; M.audio.play().catch(() => {}); } else { // Check drift const drift = Math.abs(M.audio.currentTime - data.currentTimestamp); if (drift >= 2) { M.audio.currentTime = data.currentTimestamp; } } } else if (!wasServerPaused && M.serverPaused) { // Server just paused M.audio.pause(); } } M.updateUI(); }; })();