120 lines
3.5 KiB
JavaScript
120 lines
3.5 KiB
JavaScript
/**
|
|
* API client utilities for making HTTP requests
|
|
*/
|
|
|
|
class APIError extends Error {
|
|
constructor(message, status, data) {
|
|
super(message);
|
|
this.name = 'APIError';
|
|
this.status = status;
|
|
this.data = data;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Safely parse JSON response, handling HTML error pages gracefully
|
|
*/
|
|
async function safeJsonParse(response) {
|
|
const contentType = response.headers.get('content-type') || '';
|
|
|
|
// If response is not JSON, extract useful error from HTML
|
|
if (!contentType.includes('application/json')) {
|
|
const text = await response.text();
|
|
// Try to extract error message from HTML title or h1
|
|
const titleMatch = text.match(/<title>([^<]+)<\/title>/i);
|
|
const h1Match = text.match(/<h1>([^<]+)<\/h1>/i);
|
|
const errorMsg = titleMatch?.[1] || h1Match?.[1] ||
|
|
`Server returned non-JSON response (status ${response.status})`;
|
|
throw new APIError(errorMsg, response.status, { htmlResponse: true });
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
export async function apiRequest(url, options = {}) {
|
|
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
|
|
|
const defaultOptions = {
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...(csrfToken && { 'X-CSRFToken': csrfToken })
|
|
}
|
|
};
|
|
|
|
const mergedOptions = {
|
|
...defaultOptions,
|
|
...options,
|
|
headers: {
|
|
...defaultOptions.headers,
|
|
...options.headers
|
|
}
|
|
};
|
|
|
|
try {
|
|
const response = await fetch(url, mergedOptions);
|
|
const data = await safeJsonParse(response);
|
|
|
|
if (!response.ok) {
|
|
throw new APIError(
|
|
data.error || 'Request failed',
|
|
response.status,
|
|
data
|
|
);
|
|
}
|
|
|
|
return data;
|
|
} catch (error) {
|
|
if (error instanceof APIError) {
|
|
throw error;
|
|
}
|
|
throw new APIError(error.message, 0, null);
|
|
}
|
|
}
|
|
|
|
export async function uploadFile(url, file, onProgress = null) {
|
|
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
|
const formData = new FormData();
|
|
formData.append('audio_file', file);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const xhr = new XMLHttpRequest();
|
|
|
|
if (onProgress) {
|
|
xhr.upload.addEventListener('progress', (e) => {
|
|
if (e.lengthComputable) {
|
|
const percentComplete = (e.loaded / e.total) * 100;
|
|
onProgress(percentComplete);
|
|
}
|
|
});
|
|
}
|
|
|
|
xhr.addEventListener('load', () => {
|
|
if (xhr.status >= 200 && xhr.status < 300) {
|
|
try {
|
|
const data = JSON.parse(xhr.responseText);
|
|
resolve(data);
|
|
} catch (e) {
|
|
reject(new Error('Invalid response format'));
|
|
}
|
|
} else {
|
|
try {
|
|
const error = JSON.parse(xhr.responseText);
|
|
reject(new APIError(error.error || 'Upload failed', xhr.status, error));
|
|
} catch (e) {
|
|
reject(new APIError('Upload failed', xhr.status, null));
|
|
}
|
|
}
|
|
});
|
|
|
|
xhr.addEventListener('error', () => {
|
|
reject(new Error('Network error'));
|
|
});
|
|
|
|
xhr.open('POST', url);
|
|
if (csrfToken) {
|
|
xhr.setRequestHeader('X-CSRFToken', csrfToken);
|
|
}
|
|
xhr.send(formData);
|
|
});
|
|
}
|