fix(api.js): correct capture paths, bin routes, device normalisation, upload camelCase, session tracking
This commit is contained in:
parent
a9cc8caf42
commit
31ca999075
1 changed files with 124 additions and 146 deletions
|
|
@ -3,12 +3,16 @@
|
|||
const API_BASE = '/api/v1';
|
||||
const CAPTURE_BASE = '/capture';
|
||||
|
||||
// Session ID for the active capture — set on start, cleared on stop
|
||||
let _captureSessionId = null;
|
||||
|
||||
/**
|
||||
* Wrapper around fetch with JSON parsing and error handling
|
||||
*/
|
||||
async function api(path, options = {}) {
|
||||
const url = `${API_BASE}${path}`;
|
||||
const config = {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
|
|
@ -33,11 +37,12 @@ async function api(path, options = {}) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Wrapper around fetch for capture endpoints
|
||||
* Wrapper around fetch for capture endpoints (goes directly to capture service via nginx)
|
||||
*/
|
||||
async function captureApi(path, options = {}) {
|
||||
const url = `${CAPTURE_BASE}${path}`;
|
||||
const config = {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
|
|
@ -65,32 +70,20 @@ async function captureApi(path, options = {}) {
|
|||
// 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',
|
||||
|
|
@ -98,41 +91,26 @@ async function updateAsset(assetId, data) {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Search assets by query
|
||||
*/
|
||||
async function searchAssets(query) {
|
||||
return api(`/assets/search?q=${encodeURIComponent(query)}`);
|
||||
return api(`/assets?q=${encodeURIComponent(query)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset status and metadata
|
||||
*/
|
||||
async function getAssetMetadata(assetId) {
|
||||
return api(`/assets/${assetId}/metadata`);
|
||||
return api(`/assets/${assetId}`);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 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',
|
||||
|
|
@ -140,9 +118,6 @@ async function createProject(name, description = '') {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update project metadata
|
||||
*/
|
||||
async function updateProject(projectId, data) {
|
||||
return api(`/projects/${projectId}`, {
|
||||
method: 'PATCH',
|
||||
|
|
@ -150,9 +125,6 @@ async function updateProject(projectId, data) {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a project
|
||||
*/
|
||||
async function deleteProject(projectId) {
|
||||
return api(`/projects/${projectId}`, {
|
||||
method: 'DELETE',
|
||||
|
|
@ -161,181 +133,178 @@ async function deleteProject(projectId) {
|
|||
|
||||
// ============================================================
|
||||
// BIN API CALLS
|
||||
// Bins are mounted at /api/v1/bins with project_id as query param
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Get all bins for a project
|
||||
*/
|
||||
async function getBins(projectId) {
|
||||
return api(`/projects/${projectId}/bins`);
|
||||
return api(`/bins?project_id=${projectId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single bin by ID
|
||||
*/
|
||||
async function getBin(projectId, binId) {
|
||||
return api(`/projects/${projectId}/bins/${binId}`);
|
||||
return api(`/bins/${binId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new bin in a project
|
||||
*/
|
||||
async function createBin(projectId, name, description = '') {
|
||||
return api(`/projects/${projectId}/bins`, {
|
||||
return api('/bins', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name, description }),
|
||||
body: JSON.stringify({ project_id: projectId, name }),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update bin metadata
|
||||
*/
|
||||
async function updateBin(projectId, binId, data) {
|
||||
return api(`/projects/${projectId}/bins/${binId}`, {
|
||||
return api(`/bins/${binId}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a bin
|
||||
*/
|
||||
async function deleteBin(projectId, binId) {
|
||||
return api(`/projects/${projectId}/bins/${binId}`, {
|
||||
return api(`/bins/${binId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// CAPTURE API CALLS
|
||||
// Routes: GET /capture/devices, GET /capture/status,
|
||||
// POST /capture/start, POST /capture/stop
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Get list of available capture devices
|
||||
* Get list of available capture devices.
|
||||
* Normalises capture service response ({index, name}) to {id, name, interface}.
|
||||
*/
|
||||
async function getCaptureDevices() {
|
||||
return captureApi('/devices');
|
||||
const result = await captureApi('/devices');
|
||||
if (result.success && result.data) {
|
||||
const raw = Array.isArray(result.data)
|
||||
? result.data
|
||||
: (result.data.devices || []);
|
||||
result.data = raw.map(d => ({
|
||||
id: d.index !== undefined ? d.index : d.id,
|
||||
name: d.name,
|
||||
interface: d.interface || 'DeckLink',
|
||||
}));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get capture status
|
||||
*/
|
||||
/** Get overall capture service status */
|
||||
async function getCaptureStatus() {
|
||||
return captureApi('/status');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current recording state
|
||||
*/
|
||||
/** Get current recording state (alias for getCaptureStatus) */
|
||||
async function getRecordingStatus() {
|
||||
return captureApi('/recording/status');
|
||||
return captureApi('/status');
|
||||
}
|
||||
|
||||
/**
|
||||
* Start recording
|
||||
* Start recording.
|
||||
* @param {number} deviceIndex - Device index from getCaptureDevices
|
||||
* @param {string} projectId
|
||||
* @param {string|null} binId
|
||||
* @param {string} clipName
|
||||
*/
|
||||
async function startRecording(deviceId, projectId, binId, clipName) {
|
||||
return captureApi('/recording/start', {
|
||||
async function startRecording(deviceIndex, projectId, binId, clipName) {
|
||||
const result = await captureApi('/start', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
device_id: deviceId,
|
||||
device: parseInt(deviceIndex, 10),
|
||||
project_id: projectId,
|
||||
bin_id: binId,
|
||||
clip_name: clipName,
|
||||
bin_id: binId || null,
|
||||
clip_name: clipName,
|
||||
}),
|
||||
});
|
||||
if (result.success && result.data && result.data.sessionId) {
|
||||
_captureSessionId = result.data.sessionId;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop recording
|
||||
* Stop the active recording.
|
||||
* Uses the session ID stored from the most recent startRecording call,
|
||||
* falling back to the current status if no local state exists.
|
||||
*/
|
||||
async function stopRecording() {
|
||||
return captureApi('/recording/stop', {
|
||||
let sessionId = _captureSessionId;
|
||||
|
||||
if (!sessionId) {
|
||||
// Try to recover session_id from live status
|
||||
const statusResult = await captureApi('/status');
|
||||
if (statusResult.success && statusResult.data && statusResult.data.sessionId) {
|
||||
sessionId = statusResult.data.sessionId;
|
||||
}
|
||||
}
|
||||
|
||||
const result = await captureApi('/stop', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ session_id: sessionId }),
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
_captureSessionId = null;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent capture sessions
|
||||
* Get recent captures — uses assets API ordered by created_at desc.
|
||||
*/
|
||||
async function getRecentCaptures(limit = 10) {
|
||||
return captureApi(`/sessions?limit=${limit}`);
|
||||
return api(`/assets?limit=${limit}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get timecode for current recording
|
||||
*/
|
||||
/** Not available in current capture service — returns empty */
|
||||
async function getRecordingTimecode() {
|
||||
return captureApi('/recording/timecode');
|
||||
return { success: true, data: { timecode: null } };
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 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 sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
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 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(':');
|
||||
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',
|
||||
ingesting: 'Ingesting',
|
||||
processing: 'Processing',
|
||||
ready: 'Ready',
|
||||
error: 'Error',
|
||||
archived: 'Archived',
|
||||
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);
|
||||
};
|
||||
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) {
|
||||
|
|
@ -349,35 +318,60 @@ function throttle(func, limit) {
|
|||
|
||||
// ============================================================
|
||||
// UPLOAD API CALLS
|
||||
// Routes expect camelCase field names
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Initialize a multipart upload
|
||||
* Initialize a multipart upload.
|
||||
* Returns { assetId, uploadId, key }
|
||||
*/
|
||||
async function initUpload(data) {
|
||||
return api('/upload/init', { method: 'POST', body: JSON.stringify(data) });
|
||||
const body = {
|
||||
filename: data.filename,
|
||||
fileSize: data.file_size || data.fileSize,
|
||||
contentType: data.content_type || data.contentType,
|
||||
projectId: data.project_id || data.projectId,
|
||||
binId: data.bin_id || data.binId || null,
|
||||
tags: data.tags || null,
|
||||
};
|
||||
return api('/upload/init', { method: 'POST', body: JSON.stringify(body) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete a multipart upload
|
||||
* Complete a multipart upload.
|
||||
* @param {object} data - { uploadId, key, assetId, parts: [{partNumber, ETag}] }
|
||||
*/
|
||||
async function completeUpload(data) {
|
||||
return api('/upload/complete', { method: 'POST', body: JSON.stringify(data) });
|
||||
const body = {
|
||||
uploadId: data.upload_id || data.uploadId,
|
||||
key: data.key,
|
||||
assetId: data.asset_id || data.assetId,
|
||||
parts: (data.parts || []).map(p => ({
|
||||
partNumber: p.part_number || p.partNumber,
|
||||
ETag: p.etag || p.ETag,
|
||||
})),
|
||||
};
|
||||
return api('/upload/complete', { method: 'POST', body: JSON.stringify(body) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Abort an ongoing upload
|
||||
*/
|
||||
/** Abort an ongoing multipart upload */
|
||||
async function abortUpload(data) {
|
||||
return api('/upload/abort', { method: 'POST', body: JSON.stringify(data) });
|
||||
const body = {
|
||||
uploadId: data.upload_id || data.uploadId,
|
||||
key: data.key,
|
||||
assetId: data.asset_id || data.assetId,
|
||||
};
|
||||
return api('/upload/abort', { method: 'POST', body: JSON.stringify(body) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file part (FormData, no JSON content-type)
|
||||
*/
|
||||
/** 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 });
|
||||
const response = await fetch('/api/v1/upload/part', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: formData,
|
||||
});
|
||||
if (!response.ok) throw new Error(`Upload part failed: ${response.status}`);
|
||||
const data = await response.json();
|
||||
return { success: true, data };
|
||||
|
|
@ -386,12 +380,14 @@ async function uploadPart(formData) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple upload for small files (under 50MB)
|
||||
*/
|
||||
/** Simple upload for small files (under 50MB) */
|
||||
async function simpleUpload(formData) {
|
||||
try {
|
||||
const response = await fetch('/api/v1/upload/simple', { method: 'POST', body: formData });
|
||||
const response = await fetch('/api/v1/upload/simple', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: formData,
|
||||
});
|
||||
if (!response.ok) throw new Error(`Upload failed: ${response.status}`);
|
||||
const data = await response.json();
|
||||
return { success: true, data };
|
||||
|
|
@ -404,44 +400,26 @@ async function simpleUpload(formData) {
|
|||
// 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`);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue