// Content insertion management: upload flow, placement form, duration detection. (function() { 'use strict'; // Namespace window.MNW = window.MNW || {}; function startUpload() { document.getElementById('insertion-file-input').click(); } async function handleFileSelected(input) { const file = input.files[0]; if (!file) return; // Detect duration via temporary audio element var durationMs; try { durationMs = await detectDuration(file); } catch (e) { showToast('Could not detect audio duration. Please try a different file.'); input.value = ''; return; } try { // Step 1: Get presigned URL var presignRes = await fetch('/api/users/me/insertions/presign', { method: 'POST', headers: Object.assign({ 'Content-Type': 'application/json' }, csrfHeaders()), body: JSON.stringify({ file_name: file.name, content_type: file.type || 'audio/mpeg' }) }); if (!presignRes.ok) { var err = await presignRes.json().catch(function() { return {}; }); throw new Error(err.error || 'Failed to get upload URL'); } var presignData = await presignRes.json(); // Step 2: Upload to S3 var uploadRes = await fetch(presignData.upload_url, { method: 'PUT', headers: { 'Content-Type': file.type || 'audio/mpeg' }, body: file }); if (!uploadRes.ok) { throw new Error('Upload failed'); } // Step 3: Confirm upload var title = file.name.replace(/\.[^.]+$/, ''); var confirmRes = await fetch('/api/users/me/insertions/confirm', { method: 'POST', headers: Object.assign({ 'Content-Type': 'application/json' }, csrfHeaders()), body: JSON.stringify({ s3_key: presignData.s3_key, title: title, duration_ms: durationMs, file_size: file.size, mime_type: file.type || 'audio/mpeg' }) }); if (!confirmRes.ok) { var confirmErr = await confirmRes.json().catch(function() { return {}; }); throw new Error(confirmErr.error || 'Failed to confirm upload'); } // Refresh the insertion list via HTMX htmx.ajax('GET', '/api/users/me/insertions', { target: '#insertion-library', swap: 'outerHTML' }); } catch (e) { showToast('Upload error: ' + e.message); } input.value = ''; } function detectDuration(file) { return new Promise(function(resolve, reject) { var audio = document.createElement('audio'); audio.preload = 'metadata'; audio.addEventListener('loadedmetadata', function() { var ms = Math.round(audio.duration * 1000); URL.revokeObjectURL(audio.src); resolve(ms); }); audio.addEventListener('error', function() { URL.revokeObjectURL(audio.src); reject(new Error('Cannot read audio metadata')); }); audio.src = URL.createObjectURL(file); }); } function rename(id, currentTitle) { var newTitle = prompt('New title:', currentTitle); if (!newTitle || newTitle === currentTitle) return; fetch('/api/insertions/' + id, { method: 'PUT', headers: Object.assign({ 'Content-Type': 'application/json' }, csrfHeaders()), body: JSON.stringify({ title: newTitle }) }).then(function() { htmx.ajax('GET', '/api/users/me/insertions', { target: '#insertion-library', swap: 'outerHTML' }); }); } function toggleOffsetInput(position) { var group = document.getElementById('offset-input-group'); if (group) { group.classList.toggle('hidden', position !== 'mid_roll'); } } function addPlacement(itemId) { var insertionId = document.getElementById('placement-insertion-id').value; var position = document.getElementById('placement-position').value; var offsetInput = document.getElementById('placement-offset'); var offsetMs = null; if (position === 'mid_roll') { var offsetSecs = parseInt(offsetInput.value, 10); if (isNaN(offsetSecs) || offsetSecs < 0) { showToast('Please enter a valid offset in seconds for mid-roll.'); return; } offsetMs = offsetSecs * 1000; } fetch('/api/items/' + itemId + '/insertions', { method: 'POST', headers: Object.assign({ 'Content-Type': 'application/json' }, csrfHeaders()), body: JSON.stringify({ insertion_id: insertionId, position: position, offset_ms: offsetMs, sort_order: 0 }) }).then(function(res) { if (res.ok) { htmx.ajax('GET', '/api/items/' + itemId + '/insertions', { target: '#placement-list', swap: 'outerHTML' }); } else { res.json().then(function(data) { showToast(data.error || 'Failed to add clip'); }).catch(function() { showToast('Failed to add clip'); }); } }); } MNW.insertions = { startUpload: startUpload, handleFileSelected: handleFileSelected, rename: rename, toggleOffsetInput: toggleOffsetInput, addPlacement: addPlacement }; })();