From beef39545601f71824dfc9020e6f9bc1c557530c Mon Sep 17 00:00:00 2001 From: peterino2 Date: Tue, 3 Feb 2026 01:44:15 -0800 Subject: [PATCH] saving --- .gitignore | 1 - musicroom.db | Bin 106496 -> 0 bytes public/queue.js | 196 +++++++++++++++++++++++++++++++++++++++++++----- server.ts | 2 +- 4 files changed, 179 insertions(+), 20 deletions(-) delete mode 100644 musicroom.db diff --git a/.gitignore b/.gitignore index 92ad8c0..321f6e2 100644 --- a/.gitignore +++ b/.gitignore @@ -35,5 +35,4 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json tmp/ library_cache.db - musicroom.db diff --git a/musicroom.db b/musicroom.db deleted file mode 100644 index 2397c175e0bbff9c4c17e1114aa14521aa435e49..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 106496 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 { if (t.filename) trackMap.set(t.id, t.filename); }); + M.queue.forEach(t => { if (t.filename && !trackMap.has(t.id)) trackMap.set(t.id, t.filename); }); + + // Only export tracks with known filenames + exportQueue = cachedIds + .filter(id => trackMap.has(id)) + .map(id => ({ id, filename: trackMap.get(id) })); + + const skipped = cachedIds.length - exportQueue.length; + if (exportQueue.length === 0) { + M.showToast("No exportable tracks (filenames unknown)", "warning"); + return; + } + + isExporting = true; + const msg = skipped > 0 + ? `Exporting ${exportQueue.length} tracks (${skipped} skipped - not in library)` + : `Exporting ${exportQueue.length} cached tracks...`; + M.showToast(msg); + + let exported = 0; + for (const { id, filename } of exportQueue) { + if (!isExporting) break; // Allow cancellation + + try { + const cached = await TrackStorage.get(id); + if (cached && cached.blob) { + const url = URL.createObjectURL(cached.blob); + const a = document.createElement("a"); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + exported++; + + // Small delay between downloads to not overwhelm browser + await new Promise(r => setTimeout(r, 500)); + } + } catch (e) { + console.error(`Export error for ${filename}:`, e); + } + } + + isExporting = false; + exportQueue = []; + M.showToast(`Exported ${exported} tracks`); + }; + + M.cancelExport = function() { + if (isExporting) { + isExporting = false; + M.showToast("Export cancelled"); + } + }; + // Close context menu when clicking elsewhere document.addEventListener("click", () => { if (activeContextMenu) { @@ -481,19 +613,29 @@ }); } - // Preload track(s) option + // Preload track(s) option - always show const idsToPreload = hasSelection ? [...M.selectedQueueIndices].map(idx => M.queue[idx]?.id).filter(Boolean) : [trackId]; - const uncachedIds = idsToPreload.filter(id => !M.cachedTracks.has(id)); - if (uncachedIds.length > 0) { - const preloadLabel = uncachedIds.length > 1 ? `⬇ Preload ${uncachedIds.length} tracks` : "⬇ Preload track"; - menuItems.push({ - label: preloadLabel, - action: () => { - M.showToast(`Preloading ${uncachedIds.length} track${uncachedIds.length > 1 ? 's' : ''}...`); - uncachedIds.forEach(id => M.downloadAndCacheTrack(id)); + 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)); + } + }); + + // Download track option (single track only) + if (!hasSelection) { + menuItems.push({ + label: "Download", + action: () => downloadTrack(trackId, track.filename) }); } @@ -678,16 +820,34 @@ }); } - // Preload track(s) option - const uncachedIds = idsToAdd.filter(id => !M.cachedTracks.has(id)); - if (uncachedIds.length > 0) { - const preloadLabel = uncachedIds.length > 1 ? `⬇ Preload ${uncachedIds.length} tracks` : "⬇ Preload track"; - menuItems.push({ - label: preloadLabel, - action: () => { - M.showToast(`Preloading ${uncachedIds.length} track${uncachedIds.length > 1 ? 's' : ''}...`); - uncachedIds.forEach(id => M.downloadAndCacheTrack(id)); + // 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; } + M.showToast(`Preloading ${uncachedIds.length} track${uncachedIds.length > 1 ? 's' : ''}...`); + uncachedIds.forEach(id => M.downloadAndCacheTrack(id)); + } + }); + + // Download track option (single track only) + if (!hasSelection) { + menuItems.push({ + label: "Download", + action: () => downloadTrack(track.id, track.filename) + }); + } + + // Export all cached option (if there are cached tracks) + if (M.cachedTracks.size > 0) { + menuItems.push({ + label: `Preload and export ${M.cachedTracks.size} cached`, + action: () => M.exportAllCached() }); } diff --git a/server.ts b/server.ts index 910b7ac..e1f6fdc 100644 --- a/server.ts +++ b/server.ts @@ -819,7 +819,7 @@ serve({ if (!channel) return new Response("Not found", { status: 404 }); try { const body = await req.json(); - const validModes = ["repeat-all", "repeat-one", "shuffle"]; + const validModes = ["once", "repeat-all", "repeat-one", "shuffle"]; if (typeof body.mode === "string" && validModes.includes(body.mode)) { channel.setPlaybackMode(body.mode); return Response.json({ success: true, playbackMode: channel.playbackMode });