dragonflight/services/web-ui/public/js/api.js

348 lines
7.8 KiB
JavaScript

// 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);
}
};
}