stream only mode

This commit is contained in:
peterino2 2026-02-03 01:55:57 -08:00
parent 0ad09701e5
commit 10985f391b
6 changed files with 114 additions and 33 deletions

View File

@ -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();

12
public/controls.js vendored
View File

@ -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",

View File

@ -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

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NeoRose</title>
<link rel="stylesheet" href="styles.css?v=19">
<link rel="stylesheet" href="styles.css?v=20">
</head>
<body>
<div id="app">
@ -100,6 +100,7 @@
<div id="download-speed"></div>
</div>
<div id="volume-controls">
<span id="btn-stream-only" title="Toggle stream-only mode (no caching)">stream</span>
<span id="btn-mute" title="Toggle mute">🔊</span>
<input type="range" id="volume" min="0" max="1" step="0.01" value="1">
</div>

View File

@ -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) {

View File

@ -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; }