diff --git a/public/upload.js b/public/upload.js new file mode 100644 index 0000000..d500d19 --- /dev/null +++ b/public/upload.js @@ -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`); + } + } + }); +})(); diff --git a/server.ts b/server.ts index ac5acfd..910b7ac 100644 --- a/server.ts +++ b/server.ts @@ -45,7 +45,21 @@ interface Config { } 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 PUBLIC_DIR = join(import.meta.dir, "public");