(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 =
'';
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);
})();