stream only mode
This commit is contained in:
parent
0ad09701e5
commit
10985f391b
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
Loading…
Reference in New Issue