stream only mode
This commit is contained in:
parent
0ad09701e5
commit
10985f391b
|
|
@ -4,6 +4,55 @@
|
||||||
(function() {
|
(function() {
|
||||||
const M = window.MusicRoom;
|
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
|
// Get or create cache for a track
|
||||||
M.getTrackCache = function(trackId) {
|
M.getTrackCache = function(trackId) {
|
||||||
if (!trackId) return new Set();
|
if (!trackId) return new Set();
|
||||||
|
|
@ -78,12 +127,22 @@
|
||||||
const blobUrl = URL.createObjectURL(blob);
|
const blobUrl = URL.createObjectURL(blob);
|
||||||
M.trackBlobs.set(trackId, blobUrl);
|
M.trackBlobs.set(trackId, blobUrl);
|
||||||
|
|
||||||
// Persist to storage
|
// Persist to storage (handle quota errors gracefully)
|
||||||
await TrackStorage.set(trackId, blob, contentType);
|
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
|
// Update cache status and re-render lists
|
||||||
console.log("[Cache] Track cached:", trackId.slice(0, 16) + "...", "| size:", (data.byteLength / 1024 / 1024).toFixed(2) + "MB");
|
console.log("[Cache] Track cached:", trackId.slice(0, 16) + "...", "| size:", (data.byteLength / 1024 / 1024).toFixed(2) + "MB");
|
||||||
M.cachedTracks.add(trackId);
|
|
||||||
M.renderQueue();
|
M.renderQueue();
|
||||||
M.renderLibrary();
|
M.renderLibrary();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,18 @@
|
||||||
M.$("#btn-prev").onclick = () => jumpToTrack(M.currentIndex - 1);
|
M.$("#btn-prev").onclick = () => jumpToTrack(M.currentIndex - 1);
|
||||||
M.$("#btn-next").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
|
// Playback mode button
|
||||||
const modeLabels = {
|
const modeLabels = {
|
||||||
"once": "once",
|
"once": "once",
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,8 @@ window.MusicRoom = {
|
||||||
trackBlobs: new Map(), // Map of filename -> Blob URL for fully cached tracks
|
trackBlobs: new Map(), // Map of filename -> Blob URL for fully cached tracks
|
||||||
bulkDownloadStarted: new Map(),
|
bulkDownloadStarted: new Map(),
|
||||||
cachedTracks: new Set(), // Set of track IDs that are fully cached locally
|
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
|
// Download metrics
|
||||||
audioBytesPerSecond: 20000, // Audio bitrate estimate for range requests
|
audioBytesPerSecond: 20000, // Audio bitrate estimate for range requests
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>NeoRose</title>
|
<title>NeoRose</title>
|
||||||
<link rel="stylesheet" href="styles.css?v=19">
|
<link rel="stylesheet" href="styles.css?v=20">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
|
|
@ -100,6 +100,7 @@
|
||||||
<div id="download-speed"></div>
|
<div id="download-speed"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="volume-controls">
|
<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>
|
<span id="btn-mute" title="Toggle mute">🔊</span>
|
||||||
<input type="range" id="volume" min="0" max="1" step="0.01" value="1">
|
<input type="range" id="volume" min="0" max="1" step="0.01" value="1">
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -613,23 +613,25 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preload track(s) option - always show
|
// Preload track(s) option - only show if not in stream-only mode
|
||||||
const idsToPreload = hasSelection
|
if (!M.streamOnly) {
|
||||||
? [...M.selectedQueueIndices].map(idx => M.queue[idx]?.id).filter(Boolean)
|
const idsToPreload = hasSelection
|
||||||
: [trackId];
|
? [...M.selectedQueueIndices].map(idx => M.queue[idx]?.id).filter(Boolean)
|
||||||
const preloadLabel = hasSelection && idsToPreload.length > 1 ? `Preload ${idsToPreload.length} tracks` : "Preload track";
|
: [trackId];
|
||||||
menuItems.push({
|
const preloadLabel = hasSelection && idsToPreload.length > 1 ? `Preload ${idsToPreload.length} tracks` : "Preload track";
|
||||||
label: preloadLabel,
|
menuItems.push({
|
||||||
action: () => {
|
label: preloadLabel,
|
||||||
const uncachedIds = idsToPreload.filter(id => !M.cachedTracks.has(id));
|
action: () => {
|
||||||
if (uncachedIds.length === 0) {
|
const uncachedIds = idsToPreload.filter(id => !M.cachedTracks.has(id));
|
||||||
M.showToast("All tracks already cached");
|
if (uncachedIds.length === 0) {
|
||||||
return;
|
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)
|
// Download track option (single track only)
|
||||||
if (!hasSelection) {
|
if (!hasSelection) {
|
||||||
|
|
@ -820,20 +822,22 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preload track(s) option - always show, will skip already cached
|
// Preload track(s) option - only show if not in stream-only mode
|
||||||
const preloadLabel = selectedCount > 1 ? `Preload ${selectedCount} tracks` : "Preload track";
|
if (!M.streamOnly) {
|
||||||
menuItems.push({
|
const preloadLabel = selectedCount > 1 ? `Preload ${selectedCount} tracks` : "Preload track";
|
||||||
label: preloadLabel,
|
menuItems.push({
|
||||||
action: () => {
|
label: preloadLabel,
|
||||||
const uncachedIds = idsToAdd.filter(id => !M.cachedTracks.has(id));
|
action: () => {
|
||||||
if (uncachedIds.length === 0) {
|
const uncachedIds = idsToAdd.filter(id => !M.cachedTracks.has(id));
|
||||||
M.showToast("All tracks already cached");
|
if (uncachedIds.length === 0) {
|
||||||
return;
|
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)
|
// Download track option (single track only)
|
||||||
if (!hasSelection) {
|
if (!hasSelection) {
|
||||||
|
|
|
||||||
|
|
@ -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; } }
|
@keyframes throb { from { background: #444; } to { background: #888; } }
|
||||||
#download-speed { font-size: 0.6rem; color: #555; text-align: right; }
|
#download-speed { font-size: 0.6rem; color: #555; text-align: right; }
|
||||||
#volume-controls { display: flex; gap: 0.4rem; align-items: center; }
|
#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 { font-size: 1rem; cursor: pointer; opacity: 0.8; transition: opacity 0.2s; }
|
||||||
#btn-mute:hover { opacity: 1; }
|
#btn-mute:hover { opacity: 1; }
|
||||||
#volume { width: 120px; accent-color: #4e8; }
|
#volume { width: 120px; accent-color: #4e8; }
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue