// MusicRoom - Core module // Shared state and namespace setup window.MusicRoom = { // Audio element audio: new Audio(), // WebSocket and channel state ws: null, currentChannelId: null, currentTrackId: null, currentTitle: null, serverTimestamp: 0, serverTrackDuration: 0, lastServerUpdate: 0, serverPaused: true, // Channels list channels: [], // Sync state wantSync: true, // User intent - do they want to be synced? synced: false, // Actual state - are we currently synced? // Volume preMuteVolume: 1, STORAGE_KEY: "blastoise_volume", CHANNEL_STORAGE_KEY: "blastoise_channel", THEME_STORAGE_KEY: "blastoise_theme", // Playback state localTimestamp: 0, queue: [], currentIndex: 0, playbackMode: "repeat-all", // User state currentUser: null, serverStatus: null, // Library (all discovered tracks) library: [], // Caching state prefetchController: null, loadingSegments: new Set(), trackCaches: new Map(), // Map of filename -> Set of cached segment indices trackBlobs: new Map(), // Map of filename -> Blob URL for fully cached tracks bulkDownloadStarted: new Map(), cachedTracks: new Set(), // Set of track IDs that are fully cached locally streamOnly: false, // When true, skip bulk downloads (still cache played tracks) cacheLimit: 100 * 1024 * 1024, // 100MB default cache limit // Download metrics audioBytesPerSecond: 20000, // Audio bitrate estimate for range requests downloadSpeed: 0, // Actual network download speed recentDownloads: [], // Track recent downloads for speed calculation // Constants SEGMENTS: 20, FAST_THRESHOLD: 10 * 1024 * 1024, // 10 MB/s // UI update tracking (to avoid unnecessary DOM updates) lastProgressPct: -1, lastTimeCurrent: "", lastTimeTotal: "", lastBufferPct: -1, lastSpeedText: "" }; (function() { const M = window.MusicRoom; const CHANNEL_COOKIE_MAX_AGE = 60 * 60 * 24 * 400; M.getCookieValue = function(name) { const prefix = `${name}=`; return document.cookie .split(";") .map(cookie => cookie.trim()) .find(cookie => cookie.startsWith(prefix)) ?.slice(prefix.length) || null; }; M.setCookieValue = function(name, value, maxAge = CHANNEL_COOKIE_MAX_AGE) { document.cookie = `${name}=${encodeURIComponent(value)}; Path=/; SameSite=Lax; Max-Age=${maxAge}`; }; M.clearCookieValue = function(name) { document.cookie = `${name}=; Path=/; SameSite=Lax; Max-Age=0`; }; M.getSavedChannelId = function() { const cookieValue = M.getCookieValue(M.CHANNEL_STORAGE_KEY); if (cookieValue) { try { return decodeURIComponent(cookieValue); } catch { M.clearRememberedChannel(); } } try { const storedValue = localStorage.getItem(M.CHANNEL_STORAGE_KEY); if (storedValue) { M.rememberChannel(storedValue); return storedValue; } } catch {} return null; }; M.rememberChannel = function(channelId) { if (!channelId) return; M.setCookieValue(M.CHANNEL_STORAGE_KEY, channelId); try { localStorage.setItem(M.CHANNEL_STORAGE_KEY, channelId); } catch {} }; M.clearRememberedChannel = function() { M.clearCookieValue(M.CHANNEL_STORAGE_KEY); try { localStorage.removeItem(M.CHANNEL_STORAGE_KEY); } catch {} }; })();