(function() { 'use strict'; var projectId = document.getElementById('wizard-item-content-bundle-cfg').dataset.projectId; var fileQueue = []; var nextIdx = 0; var AUDIO_EXTS = ['mp3', 'wav', 'flac', 'ogg', 'aac', 'm4a']; function getExt(name) { var dot = name.lastIndexOf('.'); return dot > 0 ? name.substring(dot + 1).toLowerCase() : ''; } function titleFromFilename(name) { var dot = name.lastIndexOf('.'); var base = dot > 0 ? name.substring(0, dot) : name; return base.replace(/[_\-]/g, ' ').replace(/\s+/g, ' ').trim(); } function detectType(name) { return AUDIO_EXTS.indexOf(getExt(name)) !== -1 ? 'audio' : 'digital'; } function addFiles(fileList) { var queueEl = document.getElementById('batch-file-queue'); var tbody = document.getElementById('batch-file-rows'); for (var i = 0; i < fileList.length; i++) { var f = fileList[i]; var idx = nextIdx++; var entry = { file: f, title: titleFromFilename(f.name), type: detectType(f.name), status: 'ready', idx: idx }; fileQueue.push(entry); var tr = document.createElement('tr'); tr.id = 'batch-row-' + idx; tr.innerHTML = '' + escapeHtml(f.name) + '' + '' + '' + 'Ready'; tbody.appendChild(tr); } queueEl.classList.remove('hidden'); } function escapeAttr(s) { return s.replace(/&/g, '&').replace(/"/g, '"').replace(//g, '>'); } function setStatus(idx, text, variant) { var el = document.querySelector('.batch-status[data-idx="' + idx + '"]'); if (el) { el.textContent = text; el.classList.remove('is-success', 'is-error'); if (variant) el.classList.add(variant); } } function setProgressBar(idx) { var el = document.querySelector('.batch-status[data-idx="' + idx + '"]'); if (!el) return {}; el.classList.remove('is-success', 'is-error'); el.innerHTML = '
' + '
' + '
' + '0%
'; return { percentEl: el.querySelector('.bp-pct'), progressBar: el.querySelector('.bp-fill') }; } function appendBundleRow(itemId, title, typeLabel) { var picker = document.getElementById('bundle-picker'); var row = document.createElement('div'); row.className = 'bundle-picker-row'; row.innerHTML = '' + '' + escapeHtml(title) + '' + '' + escapeHtml(typeLabel) + '' + ''; picker.appendChild(row); } async function uploadOne(entry) { var titleInput = document.querySelector('.batch-title[data-idx="' + entry.idx + '"]'); var typeSelect = document.querySelector('.batch-type[data-idx="' + entry.idx + '"]'); if (titleInput) entry.title = titleInput.value.trim() || entry.title; if (typeSelect) entry.type = typeSelect.value; // 1. Create item setStatus(entry.idx, 'Creating...', ''); var createRes; try { createRes = await fetch('/api/projects/' + projectId + '/items', { method: 'POST', headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()), body: new URLSearchParams({ title: entry.title, item_type: entry.type, price_cents: '0' }) }); } catch (e) { setStatus(entry.idx, 'Error: network', 'is-error'); return; } if (!createRes.ok) { var errBody = await createRes.text().catch(function() { return ''; }); var errMsg = ''; try { errMsg = JSON.parse(errBody).error || errBody; } catch (_) { errMsg = errBody; } setStatus(entry.idx, 'Error: ' + (errMsg || createRes.status), 'is-error'); return; } var itemData; try { itemData = await createRes.json(); } catch (e) { setStatus(entry.idx, 'Error: bad response', 'is-error'); return; } var itemId = itemData.id; entry.itemId = itemId; // 2. Upload file var progressEls = setProgressBar(entry.idx); try { if (entry.type === 'audio') { await uploadAudio(entry, itemId, progressEls); } else { await uploadVersion(entry, itemId, progressEls); } } catch (e) { setStatus(entry.idx, e.message || 'Upload failed', 'is-error'); return; } // 3. Done — only add to picker on success setStatus(entry.idx, 'Done', 'is-success'); appendBundleRow(itemId, entry.title, entry.type === 'audio' ? 'Audio' : 'Digital'); } async function extractError(res, fallback) { var text = await res.text().catch(function() { return ''; }); try { return JSON.parse(text).error || fallback; } catch (_) { return text || fallback; } } async function uploadAudio(entry, itemId, progressEls) { var file = entry.file; var contentType = file.type || 'audio/mpeg'; var presignRes = await fetch('/api/upload/presign', { method: 'POST', headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()), body: JSON.stringify({ item_id: itemId, file_type: 'audio', file_name: file.name, content_type: contentType, file_size_bytes: file.size }) }); if (!presignRes.ok) throw new Error(await extractError(presignRes, 'Presign failed')); var presignData = await presignRes.json(); var uploader = new S3Uploader(progressEls); await uploader.upload(presignData.upload_url, file, presignData.s3_key, contentType, presignData.cache_control); var confirmRes = await fetch('/api/upload/confirm', { method: 'POST', headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()), body: JSON.stringify({ item_id: itemId, file_type: 'audio', s3_key: presignData.s3_key }) }); if (!confirmRes.ok) throw new Error(await extractError(confirmRes, 'Confirm failed')); var confirmData = await confirmRes.json().catch(function() { return {}; }); if (confirmData && confirmData.pending_review) { showToast('Upload held for review — our scanner flagged it.', 'warning'); } } async function uploadVersion(entry, itemId, progressEls) { var file = entry.file; var contentType = file.type || 'application/octet-stream'; var verRes = await fetch('/api/items/' + itemId + '/versions', { method: 'POST', headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()), body: JSON.stringify({ version_number: '1.0', changelog: null }) }); if (!verRes.ok) throw new Error(await extractError(verRes, 'Version creation failed')); var verData = await verRes.json(); var versionId = verData.id; var presignRes = await fetch('/api/versions/' + versionId + '/upload/presign', { method: 'POST', headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()), body: JSON.stringify({ file_name: file.name, content_type: contentType }) }); if (!presignRes.ok) throw new Error(await extractError(presignRes, 'Presign failed')); var presignData = await presignRes.json(); var uploader = new S3Uploader(progressEls); await uploader.upload(presignData.upload_url, file, presignData.s3_key, contentType, presignData.cache_control); var confirmRes = await fetch('/api/versions/' + versionId + '/upload/confirm', { method: 'POST', headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()), body: JSON.stringify({ s3_key: presignData.s3_key, file_size_bytes: file.size }) }); if (!confirmRes.ok) throw new Error(await extractError(confirmRes, 'Confirm failed')); var confirmData = await confirmRes.json().catch(function() { return {}; }); if (confirmData && confirmData.pending_review) { showToast('Version upload held for review — our scanner flagged it.', 'warning'); } } async function uploadAll() { var btn = document.getElementById('batch-upload-btn'); btn.disabled = true; btn.textContent = 'Uploading...'; for (var i = 0; i < fileQueue.length; i++) { if (fileQueue[i].status === 'ready') { fileQueue[i].status = 'uploading'; await uploadOne(fileQueue[i]); fileQueue[i].status = 'done'; } } btn.disabled = false; btn.textContent = 'Upload All'; } // Wire up multi-file dropzone (can't use initDropzone — it only passes one file) var dropArea = document.getElementById('batch-upload-area'); var fileInput = document.getElementById('batch-file-input'); dropArea.addEventListener('dragover', function(e) { e.preventDefault(); dropArea.classList.add('dragover'); }); dropArea.addEventListener('dragleave', function() { dropArea.classList.remove('dragover'); }); dropArea.addEventListener('drop', function(e) { e.preventDefault(); dropArea.classList.remove('dragover'); if (e.dataTransfer.files.length > 0) addFiles(e.dataTransfer.files); }); dropArea.addEventListener('click', function() { fileInput.click(); }); fileInput.addEventListener('change', function() { if (this.files.length > 0) addFiles(this.files); this.value = ''; }); document.getElementById('batch-upload-btn').addEventListener('click', uploadAll); // Collect bundle IDs on form submit function collectBundleIds() { var ids = []; document.querySelectorAll('.bundle-check:checked').forEach(function(cb) { ids.push(cb.dataset.itemId); }); document.getElementById('bundle-item-ids').value = ids.join(','); var uids = []; document.querySelectorAll('.unlisted-check:checked').forEach(function(cb) { uids.push(cb.dataset.itemId); }); document.getElementById('unlisted-item-ids').value = uids.join(','); } document.querySelector('form').addEventListener('submit', collectBundleIds); document.querySelector('form').addEventListener('htmx:configRequest', collectBundleIds); })();