saving
This commit is contained in:
parent
d9ece80418
commit
0a67dc864e
|
|
@ -0,0 +1,145 @@
|
||||||
|
(function() {
|
||||||
|
const M = window.MusicRoom;
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const uploadBtn = M.$("#btn-upload");
|
||||||
|
const fileInput = M.$("#file-input");
|
||||||
|
const dropzone = M.$("#upload-dropzone");
|
||||||
|
const libraryPanel = M.$("#library-panel");
|
||||||
|
const progressEl = M.$("#upload-progress");
|
||||||
|
const progressBar = progressEl?.querySelector(".upload-progress-bar");
|
||||||
|
const progressText = progressEl?.querySelector(".upload-progress-text");
|
||||||
|
|
||||||
|
if (!uploadBtn || !fileInput || !dropzone) return;
|
||||||
|
|
||||||
|
// Click upload button opens file dialog
|
||||||
|
uploadBtn.onclick = () => {
|
||||||
|
if (!M.currentUser) {
|
||||||
|
M.showToast("Sign in to upload");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fileInput.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
// File input change
|
||||||
|
fileInput.onchange = () => {
|
||||||
|
if (fileInput.files.length > 0) {
|
||||||
|
uploadFiles(fileInput.files);
|
||||||
|
fileInput.value = "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Drag and drop on library panel
|
||||||
|
let dragCounter = 0;
|
||||||
|
|
||||||
|
libraryPanel.ondragenter = (e) => {
|
||||||
|
if (!M.currentUser) return;
|
||||||
|
if (!e.dataTransfer.types.includes("Files")) return;
|
||||||
|
e.preventDefault();
|
||||||
|
dragCounter++;
|
||||||
|
dropzone.classList.remove("hidden");
|
||||||
|
};
|
||||||
|
|
||||||
|
libraryPanel.ondragleave = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dragCounter--;
|
||||||
|
if (dragCounter === 0) {
|
||||||
|
dropzone.classList.add("hidden");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
libraryPanel.ondragover = (e) => {
|
||||||
|
if (!M.currentUser) return;
|
||||||
|
if (!e.dataTransfer.types.includes("Files")) return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.dataTransfer.dropEffect = "copy";
|
||||||
|
};
|
||||||
|
|
||||||
|
libraryPanel.ondrop = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dragCounter = 0;
|
||||||
|
dropzone.classList.add("hidden");
|
||||||
|
|
||||||
|
if (!M.currentUser) {
|
||||||
|
M.showToast("Sign in to upload");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = e.dataTransfer.files;
|
||||||
|
if (files.length > 0) {
|
||||||
|
uploadFiles(files);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function showProgress(current, total, filename) {
|
||||||
|
if (!progressEl) return;
|
||||||
|
const pct = Math.round((current / total) * 100);
|
||||||
|
progressBar.style.width = pct + "%";
|
||||||
|
progressText.textContent = `Uploading ${current}/${total}: ${filename}`;
|
||||||
|
progressEl.classList.remove("hidden");
|
||||||
|
uploadBtn.classList.add("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideProgress() {
|
||||||
|
if (!progressEl) return;
|
||||||
|
progressEl.classList.add("hidden");
|
||||||
|
uploadBtn.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadFiles(files) {
|
||||||
|
const validExts = [".mp3", ".ogg", ".flac", ".wav", ".m4a", ".aac", ".opus", ".wma", ".mp4"];
|
||||||
|
const audioFiles = [...files].filter(f => {
|
||||||
|
const ext = f.name.toLowerCase().match(/\.[^.]+$/)?.[0];
|
||||||
|
return ext && validExts.includes(ext);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (audioFiles.length === 0) {
|
||||||
|
M.showToast("No valid audio files");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let uploaded = 0;
|
||||||
|
let failed = 0;
|
||||||
|
let duplicates = 0;
|
||||||
|
const total = audioFiles.length;
|
||||||
|
|
||||||
|
for (let i = 0; i < audioFiles.length; i++) {
|
||||||
|
const file = audioFiles[i];
|
||||||
|
showProgress(i + 1, total, file.name);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", file);
|
||||||
|
|
||||||
|
const res = await fetch("/api/upload", {
|
||||||
|
method: "POST",
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
uploaded++;
|
||||||
|
} else if (res.status === 409) {
|
||||||
|
// File already exists
|
||||||
|
M.showToast(`Already uploaded: ${file.name}`, "warning");
|
||||||
|
duplicates++;
|
||||||
|
} else {
|
||||||
|
const err = await res.json().catch(() => ({}));
|
||||||
|
console.error(`Upload failed for ${file.name}:`, err.error || res.status);
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Upload error for ${file.name}:`, e);
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hideProgress();
|
||||||
|
|
||||||
|
if (uploaded > 0) {
|
||||||
|
M.showToast(`Uploaded ${uploaded} track${uploaded > 1 ? 's' : ''}${failed > 0 ? `, ${failed} failed` : ''}`);
|
||||||
|
} else {
|
||||||
|
M.showToast(`Upload failed`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
16
server.ts
16
server.ts
|
|
@ -45,7 +45,21 @@ interface Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
const CONFIG_PATH = join(import.meta.dir, "config.json");
|
const CONFIG_PATH = join(import.meta.dir, "config.json");
|
||||||
const config: Config = await file(CONFIG_PATH).json();
|
|
||||||
|
const DEFAULT_CONFIG: Config = {
|
||||||
|
port: 3001,
|
||||||
|
musicDir: "./music",
|
||||||
|
allowGuests: true
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create default config if missing
|
||||||
|
const configFile = file(CONFIG_PATH);
|
||||||
|
if (!(await configFile.exists())) {
|
||||||
|
console.log("[Config] Creating default config.json...");
|
||||||
|
await Bun.write(CONFIG_PATH, JSON.stringify(DEFAULT_CONFIG, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
const config: Config = await configFile.json();
|
||||||
|
|
||||||
const MUSIC_DIR = resolve(import.meta.dir, config.musicDir);
|
const MUSIC_DIR = resolve(import.meta.dir, config.musicDir);
|
||||||
const PUBLIC_DIR = join(import.meta.dir, "public");
|
const PUBLIC_DIR = join(import.meta.dir, "public");
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue