saving
This commit is contained in:
parent
0a67dc864e
commit
beef395456
|
|
@ -35,5 +35,4 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||||
|
|
||||||
tmp/
|
tmp/
|
||||||
library_cache.db
|
library_cache.db
|
||||||
|
|
||||||
musicroom.db
|
musicroom.db
|
||||||
|
|
|
||||||
BIN
musicroom.db
BIN
musicroom.db
Binary file not shown.
196
public/queue.js
196
public/queue.js
|
|
@ -15,6 +15,138 @@
|
||||||
// Context menu state
|
// Context menu state
|
||||||
let activeContextMenu = null;
|
let activeContextMenu = null;
|
||||||
|
|
||||||
|
// Download state - only one at a time
|
||||||
|
let isDownloading = false;
|
||||||
|
let exportQueue = [];
|
||||||
|
let isExporting = false;
|
||||||
|
|
||||||
|
// Download a track to user's device (uses cache if available)
|
||||||
|
async function downloadTrack(trackId, filename) {
|
||||||
|
if (isDownloading) {
|
||||||
|
M.showToast("Download already in progress", "warning");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isDownloading = true;
|
||||||
|
M.showToast(`Downloading: ${filename}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
let blob = null;
|
||||||
|
|
||||||
|
// Try to get from cache first
|
||||||
|
if (M.cachedTracks.has(trackId)) {
|
||||||
|
try {
|
||||||
|
const cached = await TrackStorage.get(trackId);
|
||||||
|
if (cached && cached.blob) {
|
||||||
|
blob = cached.blob;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Cache miss, fetching from server");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to fetching from server
|
||||||
|
if (!blob) {
|
||||||
|
const res = await fetch(`/api/tracks/${encodeURIComponent(trackId)}`);
|
||||||
|
if (!res.ok) throw new Error(`Server returned ${res.status}`);
|
||||||
|
blob = await res.blob();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!blob || blob.size === 0) {
|
||||||
|
throw new Error("Empty blob");
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = URL.createObjectURL(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);
|
||||||
|
|
||||||
|
M.showToast(`Downloaded: ${filename}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Download error:", e);
|
||||||
|
M.showToast(`Download failed: ${e.message}`, "error");
|
||||||
|
} finally {
|
||||||
|
isDownloading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export all cached tracks
|
||||||
|
M.exportAllCached = async function() {
|
||||||
|
if (isExporting) {
|
||||||
|
M.showToast("Export already in progress", "warning");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build list of cached tracks with filenames
|
||||||
|
const cachedIds = [...M.cachedTracks];
|
||||||
|
if (cachedIds.length === 0) {
|
||||||
|
M.showToast("No cached tracks to export", "warning");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find filenames from library or queue
|
||||||
|
const trackMap = new Map();
|
||||||
|
M.library.forEach(t => { 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
|
// Close context menu when clicking elsewhere
|
||||||
document.addEventListener("click", () => {
|
document.addEventListener("click", () => {
|
||||||
if (activeContextMenu) {
|
if (activeContextMenu) {
|
||||||
|
|
@ -481,19 +613,29 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preload track(s) option
|
// Preload track(s) option - always show
|
||||||
const idsToPreload = hasSelection
|
const idsToPreload = hasSelection
|
||||||
? [...M.selectedQueueIndices].map(idx => M.queue[idx]?.id).filter(Boolean)
|
? [...M.selectedQueueIndices].map(idx => M.queue[idx]?.id).filter(Boolean)
|
||||||
: [trackId];
|
: [trackId];
|
||||||
const uncachedIds = idsToPreload.filter(id => !M.cachedTracks.has(id));
|
const preloadLabel = hasSelection && idsToPreload.length > 1 ? `Preload ${idsToPreload.length} tracks` : "Preload track";
|
||||||
if (uncachedIds.length > 0) {
|
menuItems.push({
|
||||||
const preloadLabel = uncachedIds.length > 1 ? `⬇ Preload ${uncachedIds.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) {
|
||||||
M.showToast(`Preloading ${uncachedIds.length} track${uncachedIds.length > 1 ? 's' : ''}...`);
|
M.showToast("All tracks already cached");
|
||||||
uncachedIds.forEach(id => M.downloadAndCacheTrack(id));
|
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
|
// Preload track(s) option - always show, will skip already cached
|
||||||
const uncachedIds = idsToAdd.filter(id => !M.cachedTracks.has(id));
|
const preloadLabel = selectedCount > 1 ? `Preload ${selectedCount} tracks` : "Preload track";
|
||||||
if (uncachedIds.length > 0) {
|
menuItems.push({
|
||||||
const preloadLabel = uncachedIds.length > 1 ? `⬇ Preload ${uncachedIds.length} tracks` : "⬇ Preload track";
|
label: preloadLabel,
|
||||||
menuItems.push({
|
action: () => {
|
||||||
label: preloadLabel,
|
const uncachedIds = idsToAdd.filter(id => !M.cachedTracks.has(id));
|
||||||
action: () => {
|
if (uncachedIds.length === 0) {
|
||||||
M.showToast(`Preloading ${uncachedIds.length} track${uncachedIds.length > 1 ? 's' : ''}...`);
|
M.showToast("All tracks already cached");
|
||||||
uncachedIds.forEach(id => M.downloadAndCacheTrack(id));
|
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()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -819,7 +819,7 @@ serve({
|
||||||
if (!channel) return new Response("Not found", { status: 404 });
|
if (!channel) return new Response("Not found", { status: 404 });
|
||||||
try {
|
try {
|
||||||
const body = await req.json();
|
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)) {
|
if (typeof body.mode === "string" && validModes.includes(body.mode)) {
|
||||||
channel.setPlaybackMode(body.mode);
|
channel.setPlaybackMode(body.mode);
|
||||||
return Response.json({ success: true, playbackMode: channel.playbackMode });
|
return Response.json({ success: true, playbackMode: channel.playbackMode });
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue