// Wild Dragon MAM - API Helper Module const API_BASE = '/api/v1'; const CAPTURE_BASE = '/capture'; /** * Wrapper around fetch with JSON parsing and error handling */ async function api(path, options = {}) { const url = `${API_BASE}${path}`; const config = { headers: { 'Content-Type': 'application/json', ...options.headers, }, ...options, }; try { const response = await fetch(url, config); if (!response.ok) { const error = await response.json().catch(() => ({ message: response.statusText })); throw new Error(error.message || `API Error: ${response.status}`); } const data = await response.json(); return { success: true, data }; } catch (error) { console.error(`API Error on ${path}:`, error); return { success: false, error: error.message }; } } /** * Wrapper around fetch for capture endpoints */ async function captureApi(path, options = {}) { const url = `${CAPTURE_BASE}${path}`; const config = { headers: { 'Content-Type': 'application/json', ...options.headers, }, ...options, }; try { const response = await fetch(url, config); if (!response.ok) { const error = await response.json().catch(() => ({ message: response.statusText })); throw new Error(error.message || `Capture API Error: ${response.status}`); } const data = await response.json(); return { success: true, data }; } catch (error) { console.error(`Capture API Error on ${path}:`, error); return { success: false, error: error.message }; } } // ============================================================ // ASSET API CALLS // ============================================================ /** * Get all assets with optional filtering */ async function getAssets(filters = {}) { const params = new URLSearchParams(filters); const path = `/assets${params.toString() ? '?' + params.toString() : ''}`; return api(path); } /** * Get a single asset by ID */ async function getAsset(assetId) { return api(`/assets/${assetId}`); } /** * Get signed streaming URL for an asset */ async function getAssetStreamUrl(assetId) { return api(`/assets/${assetId}/stream`); } /** * Update asset metadata (tags, notes, etc) */ async function updateAsset(assetId, data) { return api(`/assets/${assetId}`, { method: 'PATCH', body: JSON.stringify(data), }); } /** * Search assets by query */ async function searchAssets(query) { return api(`/assets/search?q=${encodeURIComponent(query)}`); } /** * Get asset status and metadata */ async function getAssetMetadata(assetId) { return api(`/assets/${assetId}/metadata`); } // ============================================================ // PROJECT API CALLS // ============================================================ /** * Get all projects */ async function getProjects() { return api('/projects'); } /** * Get a single project by ID */ async function getProject(projectId) { return api(`/projects/${projectId}`); } /** * Create a new project */ async function createProject(name, description = '') { return api('/projects', { method: 'POST', body: JSON.stringify({ name, description }), }); } /** * Update project metadata */ async function updateProject(projectId, data) { return api(`/projects/${projectId}`, { method: 'PATCH', body: JSON.stringify(data), }); } /** * Delete a project */ async function deleteProject(projectId) { return api(`/projects/${projectId}`, { method: 'DELETE', }); } // ============================================================ // BIN API CALLS // ============================================================ /** * Get all bins for a project */ async function getBins(projectId) { return api(`/projects/${projectId}/bins`); } /** * Get a single bin by ID */ async function getBin(projectId, binId) { return api(`/projects/${projectId}/bins/${binId}`); } /** * Create a new bin in a project */ async function createBin(projectId, name, description = '') { return api(`/projects/${projectId}/bins`, { method: 'POST', body: JSON.stringify({ name, description }), }); } /** * Update bin metadata */ async function updateBin(projectId, binId, data) { return api(`/projects/${projectId}/bins/${binId}`, { method: 'PATCH', body: JSON.stringify(data), }); } /** * Delete a bin */ async function deleteBin(projectId, binId) { return api(`/projects/${projectId}/bins/${binId}`, { method: 'DELETE', }); } // ============================================================ // CAPTURE API CALLS // ============================================================ /** * Get list of available capture devices */ async function getCaptureDevices() { return captureApi('/devices'); } /** * Get capture status */ async function getCaptureStatus() { return captureApi('/status'); } /** * Get current recording state */ async function getRecordingStatus() { return captureApi('/recording/status'); } /** * Start recording */ async function startRecording(deviceId, projectId, binId, clipName) { return captureApi('/recording/start', { method: 'POST', body: JSON.stringify({ device_id: deviceId, project_id: projectId, bin_id: binId, clip_name: clipName, }), }); } /** * Stop recording */ async function stopRecording() { return captureApi('/recording/stop', { method: 'POST', }); } /** * Get recent capture sessions */ async function getRecentCaptures(limit = 10) { return captureApi(`/sessions?limit=${limit}`); } /** * Get timecode for current recording */ async function getRecordingTimecode() { return captureApi('/recording/timecode'); } // ============================================================ // UTILITY FUNCTIONS // ============================================================ /** * Format bytes to human-readable size */ function formatFileSize(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]; } /** * Format duration in seconds to HH:MM:SS */ function formatDuration(seconds) { if (!seconds || seconds < 0) return '00:00:00'; const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const secs = Math.floor(seconds % 60); return [hours, minutes, secs] .map(v => v.toString().padStart(2, '0')) .join(':'); } /** * Get status badge text from status code */ function getStatusLabel(status) { const labels = { ingesting: 'Ingesting', processing: 'Processing', ready: 'Ready', error: 'Error', archived: 'Archived', }; return labels[status] || status; } /** * Get status badge CSS class from status code */ function getStatusBadgeClass(status) { return `badge-${status}`; } /** * Debounce function for search */ function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } /** * Throttle function for scroll/resize events */ function throttle(func, limit) { let inThrottle; return function(...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; } // ============================================================ // UPLOAD API CALLS // ============================================================ /** * Initialize a multipart upload */ async function initUpload(data) { return api('/upload/init', { method: 'POST', body: JSON.stringify(data) }); } /** * Complete a multipart upload */ async function completeUpload(data) { return api('/upload/complete', { method: 'POST', body: JSON.stringify(data) }); } /** * Abort an ongoing upload */ async function abortUpload(data) { return api('/upload/abort', { method: 'POST', body: JSON.stringify(data) }); } /** * Upload a file part (FormData, no JSON content-type) */ async function uploadPart(formData) { try { const response = await fetch('/api/v1/upload/part', { method: 'POST', body: formData }); if (!response.ok) throw new Error(`Upload part failed: ${response.status}`); const data = await response.json(); return { success: true, data }; } catch (error) { return { success: false, error: error.message }; } } /** * Simple upload for small files (under 50MB) */ async function simpleUpload(formData) { try { const response = await fetch('/api/v1/upload/simple', { method: 'POST', body: formData }); if (!response.ok) throw new Error(`Upload failed: ${response.status}`); const data = await response.json(); return { success: true, data }; } catch (error) { return { success: false, error: error.message }; } } // ============================================================ // RECORDER API CALLS // ============================================================ /** * Get all recorder instances */ async function getRecorders() { return api('/recorders'); } /** * Create a new recorder instance */ async function createRecorder(data) { return api('/recorders', { method: 'POST', body: JSON.stringify(data) }); } /** * Start a recorder by ID */ async function startRecorder(id) { return api(`/recorders/${id}/start`, { method: 'POST' }); } /** * Stop a recorder by ID */ async function stopRecorder(id) { return api(`/recorders/${id}/stop`, { method: 'POST' }); } /** * Delete a recorder by ID */ async function deleteRecorder(id) { return api(`/recorders/${id}`, { method: 'DELETE' }); } /** * Get live status for a recorder */ async function getRecorderStatus(id) { return api(`/recorders/${id}/status`); }