From 10985f391b5a48545608e0656dc2b09b4e7e375f Mon Sep 17 00:00:00 2001 From: peterino2 Date: Tue, 3 Feb 2026 01:55:57 -0800 Subject: [PATCH] stream only mode --- public/audioCache.js | 65 ++++++++++++++++++++++++++++++++++++++++++-- public/controls.js | 12 ++++++++ public/core.js | 2 ++ public/index.html | 3 +- public/queue.js | 62 ++++++++++++++++++++++-------------------- public/styles.css | 3 ++ 6 files changed, 114 insertions(+), 33 deletions(-) diff --git a/public/audioCache.js b/public/audioCache.js index ebccca7..7a30a87 100644 --- a/public/audioCache.js +++ b/public/audioCache.js @@ -4,6 +4,55 @@ (function() { const M = window.MusicRoom; + // Load stream-only preference from localStorage + M.streamOnly = localStorage.getItem("musicroom_streamOnly") === "true"; + + // Toggle stream-only mode + M.setStreamOnly = function(enabled) { + M.streamOnly = enabled; + localStorage.setItem("musicroom_streamOnly", enabled ? "true" : "false"); + M.showToast(enabled ? "Stream-only mode enabled" : "Caching enabled"); + }; + + // Check cache size and prune if over limit + M.checkCacheSize = async function() { + const stats = await TrackStorage.getStats(); + if (stats.totalSize > M.cacheLimit) { + console.log(`[Cache] Size ${(stats.totalSize / 1024 / 1024).toFixed(1)}MB exceeds limit ${(M.cacheLimit / 1024 / 1024).toFixed(0)}MB, pruning...`); + await M.pruneCache(stats.totalSize - M.cacheLimit + (10 * 1024 * 1024)); // Free extra 10MB + } + }; + + // Remove oldest cached tracks to free up space + M.pruneCache = async function(bytesToFree) { + // Get all cached tracks with metadata + const allKeys = await TrackStorage.list(); + if (allKeys.length === 0) return; + + // Get track sizes (we need to fetch each to know size) + const trackSizes = []; + for (const key of allKeys) { + const cached = await TrackStorage.get(key); + if (cached && cached.blob) { + trackSizes.push({ id: key, size: cached.blob.size }); + } + } + + // Sort by... we don't have timestamps, so just remove from start + let freed = 0; + for (const { id, size } of trackSizes) { + if (freed >= bytesToFree) break; + await TrackStorage.remove(id); + M.cachedTracks.delete(id); + freed += size; + console.log(`[Cache] Pruned: ${id.slice(0, 16)}... (${(size / 1024 / 1024).toFixed(1)}MB)`); + } + + M.showToast(`Freed ${(freed / 1024 / 1024).toFixed(0)}MB cache space`); + M.renderQueue(); + M.renderLibrary(); + }; + // Get or create cache for a track M.getTrackCache = function(trackId) { if (!trackId) return new Set(); @@ -78,12 +127,22 @@ const blobUrl = URL.createObjectURL(blob); M.trackBlobs.set(trackId, blobUrl); - // Persist to storage - await TrackStorage.set(trackId, blob, contentType); + // Persist to storage (handle quota errors gracefully) + try { + await TrackStorage.set(trackId, blob, contentType); + M.cachedTracks.add(trackId); + // Check if we need to prune old tracks + M.checkCacheSize(); + } catch (e) { + if (e.name === 'QuotaExceededError') { + M.showToast("Cache full - track will stream only", "warning"); + } else { + console.warn("[Cache] Storage error:", e); + } + } // Update cache status and re-render lists console.log("[Cache] Track cached:", trackId.slice(0, 16) + "...", "| size:", (data.byteLength / 1024 / 1024).toFixed(2) + "MB"); - M.cachedTracks.add(trackId); M.renderQueue(); M.renderLibrary(); diff --git a/public/controls.js b/public/controls.js index 26d3523..a73e85b 100644 --- a/public/controls.js +++ b/public/controls.js @@ -96,6 +96,18 @@ M.$("#btn-prev").onclick = () => jumpToTrack(M.currentIndex - 1); M.$("#btn-next").onclick = () => jumpToTrack(M.currentIndex + 1); + // Stream-only toggle + const streamBtn = M.$("#btn-stream-only"); + M.updateStreamOnlyButton = function() { + streamBtn.classList.toggle("active", M.streamOnly); + streamBtn.title = M.streamOnly ? "Stream-only mode (click to enable caching)" : "Click to enable stream-only mode (no caching)"; + }; + streamBtn.onclick = () => { + M.setStreamOnly(!M.streamOnly); + M.updateStreamOnlyButton(); + }; + M.updateStreamOnlyButton(); + // Playback mode button const modeLabels = { "once": "once", diff --git a/public/core.js b/public/core.js index 18b8992..bcef3ad 100644 --- a/public/core.js +++ b/public/core.js @@ -46,6 +46,8 @@ window.MusicRoom = { 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 diff --git a/public/index.html b/public/index.html index b551641..aaea4a9 100644 --- a/public/index.html +++ b/public/index.html @@ -4,7 +4,7 @@ NeoRose - +
@@ -100,6 +100,7 @@
+ stream 🔊
diff --git a/public/queue.js b/public/queue.js index 91287d0..e9da53b 100644 --- a/public/queue.js +++ b/public/queue.js @@ -613,23 +613,25 @@ }); } - // Preload track(s) option - always show - const idsToPreload = hasSelection - ? [...M.selectedQueueIndices].map(idx => M.queue[idx]?.id).filter(Boolean) - : [trackId]; - const preloadLabel = hasSelection && idsToPreload.length > 1 ? `Preload ${idsToPreload.length} tracks` : "Preload track"; - menuItems.push({ - label: preloadLabel, - action: () => { - const uncachedIds = idsToPreload.filter(id => !M.cachedTracks.has(id)); - if (uncachedIds.length === 0) { - M.showToast("All tracks already cached"); - return; + // Preload track(s) option - only show if not in stream-only mode + if (!M.streamOnly) { + const idsToPreload = hasSelection + ? [...M.selectedQueueIndices].map(idx => M.queue[idx]?.id).filter(Boolean) + : [trackId]; + const preloadLabel = hasSelection && idsToPreload.length > 1 ? `Preload ${idsToPreload.length} tracks` : "Preload track"; + menuItems.push({ + label: preloadLabel, + action: () => { + const uncachedIds = idsToPreload.filter(id => !M.cachedTracks.has(id)); + if (uncachedIds.length === 0) { + M.showToast("All tracks already cached"); + return; + } + M.showToast(`Preloading ${uncachedIds.length} track${uncachedIds.length > 1 ? 's' : ''}...`); + uncachedIds.forEach(id => M.downloadAndCacheTrack(id)); } - M.showToast(`Preloading ${uncachedIds.length} track${uncachedIds.length > 1 ? 's' : ''}...`); - uncachedIds.forEach(id => M.downloadAndCacheTrack(id)); - } - }); + }); + } // Download track option (single track only) if (!hasSelection) { @@ -820,20 +822,22 @@ }); } - // Preload track(s) option - always show, will skip already cached - const preloadLabel = selectedCount > 1 ? `Preload ${selectedCount} tracks` : "Preload track"; - menuItems.push({ - label: preloadLabel, - action: () => { - const uncachedIds = idsToAdd.filter(id => !M.cachedTracks.has(id)); - if (uncachedIds.length === 0) { - M.showToast("All tracks already cached"); - return; + // Preload track(s) option - only show if not in stream-only mode + if (!M.streamOnly) { + const preloadLabel = selectedCount > 1 ? `Preload ${selectedCount} tracks` : "Preload track"; + menuItems.push({ + label: preloadLabel, + action: () => { + const uncachedIds = idsToAdd.filter(id => !M.cachedTracks.has(id)); + if (uncachedIds.length === 0) { + M.showToast("All tracks already cached"); + return; + } + M.showToast(`Preloading ${uncachedIds.length} track${uncachedIds.length > 1 ? 's' : ''}...`); + uncachedIds.forEach(id => M.downloadAndCacheTrack(id)); } - M.showToast(`Preloading ${uncachedIds.length} track${uncachedIds.length > 1 ? 's' : ''}...`); - uncachedIds.forEach(id => M.downloadAndCacheTrack(id)); - } - }); + }); + } // Download track option (single track only) if (!hasSelection) { diff --git a/public/styles.css b/public/styles.css index 7e45abd..85d29af 100644 --- a/public/styles.css +++ b/public/styles.css @@ -136,6 +136,9 @@ h3 { font-size: 0.8rem; color: #666; margin-bottom: 0.3rem; text-transform: uppe @keyframes throb { from { background: #444; } to { background: #888; } } #download-speed { font-size: 0.6rem; color: #555; text-align: right; } #volume-controls { display: flex; gap: 0.4rem; align-items: center; } +#btn-stream-only { font-size: 0.7rem; cursor: pointer; color: #666; transition: color 0.2s, text-shadow 0.2s; letter-spacing: 0.05em; } +#btn-stream-only:hover { color: #888; } +#btn-stream-only.active { color: #4af; text-shadow: 0 0 6px #4af; } #btn-mute { font-size: 1rem; cursor: pointer; opacity: 0.8; transition: opacity 0.2s; } #btn-mute:hover { opacity: 1; } #volume { width: 120px; accent-color: #4e8; }