| 1 |
|
| 2 |
(function() { |
| 3 |
'use strict'; |
| 4 |
|
| 5 |
|
| 6 |
window.MNW = window.MNW || {}; |
| 7 |
|
| 8 |
function startUpload() { |
| 9 |
document.getElementById('insertion-file-input').click(); |
| 10 |
} |
| 11 |
|
| 12 |
async function handleFileSelected(input) { |
| 13 |
const file = input.files[0]; |
| 14 |
if (!file) return; |
| 15 |
|
| 16 |
|
| 17 |
var durationMs; |
| 18 |
try { |
| 19 |
durationMs = await detectDuration(file); |
| 20 |
} catch (e) { |
| 21 |
showToast('Could not detect audio duration. Please try a different file.'); |
| 22 |
input.value = ''; |
| 23 |
return; |
| 24 |
} |
| 25 |
|
| 26 |
try { |
| 27 |
|
| 28 |
var presignRes = await fetch('/api/users/me/insertions/presign', { |
| 29 |
method: 'POST', |
| 30 |
headers: Object.assign({ 'Content-Type': 'application/json' }, csrfHeaders()), |
| 31 |
body: JSON.stringify({ |
| 32 |
file_name: file.name, |
| 33 |
content_type: file.type || 'audio/mpeg' |
| 34 |
}) |
| 35 |
}); |
| 36 |
|
| 37 |
if (!presignRes.ok) { |
| 38 |
var err = await presignRes.json().catch(function() { return {}; }); |
| 39 |
throw new Error(err.error || 'Failed to get upload URL'); |
| 40 |
} |
| 41 |
|
| 42 |
var presignData = await presignRes.json(); |
| 43 |
|
| 44 |
|
| 45 |
var uploadRes = await fetch(presignData.upload_url, { |
| 46 |
method: 'PUT', |
| 47 |
headers: { 'Content-Type': file.type || 'audio/mpeg' }, |
| 48 |
body: file |
| 49 |
}); |
| 50 |
|
| 51 |
if (!uploadRes.ok) { |
| 52 |
throw new Error('Upload failed'); |
| 53 |
} |
| 54 |
|
| 55 |
|
| 56 |
var title = file.name.replace(/\.[^.]+$/, ''); |
| 57 |
var confirmRes = await fetch('/api/users/me/insertions/confirm', { |
| 58 |
method: 'POST', |
| 59 |
headers: Object.assign({ 'Content-Type': 'application/json' }, csrfHeaders()), |
| 60 |
body: JSON.stringify({ |
| 61 |
s3_key: presignData.s3_key, |
| 62 |
title: title, |
| 63 |
duration_ms: durationMs, |
| 64 |
file_size: file.size, |
| 65 |
mime_type: file.type || 'audio/mpeg' |
| 66 |
}) |
| 67 |
}); |
| 68 |
|
| 69 |
if (!confirmRes.ok) { |
| 70 |
var confirmErr = await confirmRes.json().catch(function() { return {}; }); |
| 71 |
throw new Error(confirmErr.error || 'Failed to confirm upload'); |
| 72 |
} |
| 73 |
|
| 74 |
|
| 75 |
htmx.ajax('GET', '/api/users/me/insertions', { target: '#insertion-library', swap: 'outerHTML' }); |
| 76 |
} catch (e) { |
| 77 |
showToast('Upload error: ' + e.message); |
| 78 |
} |
| 79 |
|
| 80 |
input.value = ''; |
| 81 |
} |
| 82 |
|
| 83 |
function detectDuration(file) { |
| 84 |
return new Promise(function(resolve, reject) { |
| 85 |
var audio = document.createElement('audio'); |
| 86 |
audio.preload = 'metadata'; |
| 87 |
|
| 88 |
audio.addEventListener('loadedmetadata', function() { |
| 89 |
var ms = Math.round(audio.duration * 1000); |
| 90 |
URL.revokeObjectURL(audio.src); |
| 91 |
resolve(ms); |
| 92 |
}); |
| 93 |
|
| 94 |
audio.addEventListener('error', function() { |
| 95 |
URL.revokeObjectURL(audio.src); |
| 96 |
reject(new Error('Cannot read audio metadata')); |
| 97 |
}); |
| 98 |
|
| 99 |
audio.src = URL.createObjectURL(file); |
| 100 |
}); |
| 101 |
} |
| 102 |
|
| 103 |
function rename(id, currentTitle) { |
| 104 |
var newTitle = prompt('New title:', currentTitle); |
| 105 |
if (!newTitle || newTitle === currentTitle) return; |
| 106 |
|
| 107 |
fetch('/api/insertions/' + id, { |
| 108 |
method: 'PUT', |
| 109 |
headers: Object.assign({ 'Content-Type': 'application/json' }, csrfHeaders()), |
| 110 |
body: JSON.stringify({ title: newTitle }) |
| 111 |
}).then(function() { |
| 112 |
htmx.ajax('GET', '/api/users/me/insertions', { target: '#insertion-library', swap: 'outerHTML' }); |
| 113 |
}); |
| 114 |
} |
| 115 |
|
| 116 |
function toggleOffsetInput(position) { |
| 117 |
var group = document.getElementById('offset-input-group'); |
| 118 |
if (group) { |
| 119 |
group.classList.toggle('hidden', position !== 'mid_roll'); |
| 120 |
} |
| 121 |
} |
| 122 |
|
| 123 |
function addPlacement(itemId) { |
| 124 |
var insertionId = document.getElementById('placement-insertion-id').value; |
| 125 |
var position = document.getElementById('placement-position').value; |
| 126 |
var offsetInput = document.getElementById('placement-offset'); |
| 127 |
var offsetMs = null; |
| 128 |
|
| 129 |
if (position === 'mid_roll') { |
| 130 |
var offsetSecs = parseInt(offsetInput.value, 10); |
| 131 |
if (isNaN(offsetSecs) || offsetSecs < 0) { |
| 132 |
showToast('Please enter a valid offset in seconds for mid-roll.'); |
| 133 |
return; |
| 134 |
} |
| 135 |
offsetMs = offsetSecs * 1000; |
| 136 |
} |
| 137 |
|
| 138 |
fetch('/api/items/' + itemId + '/insertions', { |
| 139 |
method: 'POST', |
| 140 |
headers: Object.assign({ 'Content-Type': 'application/json' }, csrfHeaders()), |
| 141 |
body: JSON.stringify({ |
| 142 |
insertion_id: insertionId, |
| 143 |
position: position, |
| 144 |
offset_ms: offsetMs, |
| 145 |
sort_order: 0 |
| 146 |
}) |
| 147 |
}).then(function(res) { |
| 148 |
if (res.ok) { |
| 149 |
htmx.ajax('GET', '/api/items/' + itemId + '/insertions', { target: '#placement-list', swap: 'outerHTML' }); |
| 150 |
} else { |
| 151 |
res.json().then(function(data) { |
| 152 |
showToast(data.error || 'Failed to add clip'); |
| 153 |
}).catch(function() { |
| 154 |
showToast('Failed to add clip'); |
| 155 |
}); |
| 156 |
} |
| 157 |
}); |
| 158 |
} |
| 159 |
|
| 160 |
MNW.insertions = { |
| 161 |
startUpload: startUpload, |
| 162 |
handleFileSelected: handleFileSelected, |
| 163 |
rename: rename, |
| 164 |
toggleOffsetInput: toggleOffsetInput, |
| 165 |
addPlacement: addPlacement |
| 166 |
}; |
| 167 |
})(); |
| 168 |
|