Skip to main content

max / makenotwork

5.8 KB · 168 lines History Blame Raw
1 // Content insertion management: upload flow, placement form, duration detection.
2 (function() {
3 'use strict';
4
5 // Namespace
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 // Detect duration via temporary audio element
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 // Step 1: Get presigned URL
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 // Step 2: Upload to S3
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 // Step 3: Confirm upload
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 // Refresh the insertion list via HTMX
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