/** * Upload dropzone: discard duplicate files in the selection (and known server fingerprints) * before submitting the form. */ const MAX_FILE_BYTES = 2 * 1024 * 1024 * 1024; export function uploadDropzone({ existingFingerprints = [] } = {}) { const acceptExt = ['.mp3', '.wav', '.ogg', '.oga', '.flac', '.m4a', '.mp4', '.aac', '.webm', '.wma', '.aiff', '.aif']; const known = new Set(existingFingerprints); return { files: [], dragging: false, uploading: false, error: null, notice: null, get uploadLabel() { if (this.uploading) { return 'Uploading…'; } if (this.files.length <= 1) { return 'Upload'; } return 'Upload ' + this.files.length + ' files'; }, onBrowse(event) { this.addFiles(Array.from(event.target.files || [])); }, onDrop(event) { this.dragging = false; this.addFiles(Array.from(event.dataTransfer?.files || [])); }, addFiles(incoming) { this.error = null; this.notice = null; const accepted = []; let skippedUnsupported = 0; let skippedTooLarge = 0; let skippedDuplicates = 0; for (const file of incoming) { if (! this.isAccepted(file)) { skippedUnsupported++; continue; } if (file.size > MAX_FILE_BYTES) { skippedTooLarge++; continue; } const fingerprint = this.fingerprint(file); if (known.has(fingerprint) || this.files.some((existing) => this.fingerprint(existing) === fingerprint)) { skippedDuplicates++; continue; } if (accepted.some((existing) => this.fingerprint(existing) === fingerprint)) { skippedDuplicates++; continue; } accepted.push(file); } this.files = [...this.files, ...accepted]; if (this.files.length > 50) { this.error = 'You can upload at most 50 files at once.'; this.files = this.files.slice(0, 50); } if (skippedUnsupported > 0) { this.error = 'Skipped unsupported file type. Use common audio formats only.'; } else if (skippedTooLarge > 0) { this.error = 'Skipped a file larger than 2 GB.'; } if (skippedDuplicates > 0) { this.notice = skippedDuplicates === 1 ? 'Skipped 1 duplicate file.' : `Skipped ${skippedDuplicates} duplicate files.`; } this.syncInput(); }, isAccepted(file) { const name = (file.name || '').toLowerCase(); if (acceptExt.some((ext) => name.endsWith(ext))) { return true; } return (file.type || '').startsWith('audio/'); }, fingerprint(file) { return `${String(file.name || '').toLowerCase()}:${Number(file.size) || 0}`; }, fileListKey(file, index = 0) { return `${this.fingerprint(file)}:${index}`; }, removeFile(index) { this.files.splice(index, 1); this.syncInput(); }, clearFiles() { this.files = []; this.notice = null; this.syncInput(); }, syncInput() { const input = this.$refs.fileInput; if (! input) { return; } const transfer = new DataTransfer(); this.files.forEach((file) => transfer.items.add(file)); input.files = transfer.files; }, ensureFilesSelected(event) { if (this.files.length === 0) { event.preventDefault(); this.error = 'Drop or choose at least one audio file.'; return; } this.uploading = true; this.error = null; }, formatSize(bytes) { if (bytes < 1024) { return bytes + ' B'; } if (bytes < 1024 * 1024) { return (bytes / 1024).toFixed(1) + ' KB'; } if (bytes < 1024 * 1024 * 1024) { return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; } return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB'; }, }; }