/** * Fetch-based API client for the media.rip() backend. * * All routes are relative — the Vite dev proxy handles /api → backend. * In production, the SPA is served by the same FastAPI process, so * relative paths work without configuration. */ import type { Job, JobCreate, FormatInfo, PublicConfig, HealthStatus } from './types' class ApiError extends Error { constructor( public status: number, public statusText: string, public body: string, ) { super(`API error ${status}: ${statusText}`) this.name = 'ApiError' } } async function request(url: string, options?: RequestInit): Promise { const res = await fetch(url, { ...options, headers: { 'Content-Type': 'application/json', ...options?.headers, }, }) if (!res.ok) { const body = await res.text() throw new ApiError(res.status, res.statusText, body) } // 204 No Content if (res.status === 204) { return undefined as T } return res.json() } export const api = { /** Fetch all downloads for the current session. */ async getDownloads(): Promise { return request('/api/downloads') }, /** Submit a new download. */ async createDownload(payload: JobCreate): Promise { return request('/api/downloads', { method: 'POST', body: JSON.stringify(payload), }) }, /** Cancel / remove a download. */ async deleteDownload(id: string): Promise { return request(`/api/downloads/${id}`, { method: 'DELETE', }) }, /** Extract available formats for a URL. */ async getFormats(url: string): Promise { const encoded = encodeURIComponent(url) return request(`/api/formats?url=${encoded}`) }, /** Load public (non-sensitive) configuration. */ async getPublicConfig(): Promise { return request('/api/config/public') }, /** Health check. */ async getHealth(): Promise { return request('/api/health') }, } export { ApiError }