/** * Typed API client for the FastAPI backend. */ import { getAccessToken } from "../auth"; import type { MediaCounts, LibraryCount, UserDirectoryResponse, UserMessageResponse, UserMessageQueueStatus, NowPlayingSession, MonitoringPollerStatus, MonitoringOverviewResponse, AppVersionInfo, MonitoringStatus, MonitoringMetrics, DiskSpace, SSHKey, SSHKeyInput, SSHKeyGenerated, SavedTask, SavedTaskInput, SavedTaskRun, MonitoringMachine, MonitoringMachineInput, MonitoringMachineAction, MediaIndexStatus, MediaIndexActionResponse, MediaQueryResponse, DirectoryListing, JobTemplate, JobResult, ResolvedPath, ResetLocalDatabaseInput, ResetLocalDatabaseResponse, SSHValidationResult, DashboardShortcut, DashboardShortcutInput, } from "../types"; const BASE_URL = import.meta.env.VITE_API_URL || "/api"; function isAbsoluteUrl(value: string): boolean { return /^https?:\/\//i.test(value) || value.startsWith("//"); } function buildUrl(path: string, params?: Record): string { if (!isAbsoluteUrl(BASE_URL)) { const url = new URL(path, window.location.origin); if (params) { Object.entries(params).forEach(([key, value]) => { if (value !== undefined && value !== "") url.searchParams.set(key, value); }); } return url.toString(); } const url = new URL(path, BASE_URL); if (params) { Object.entries(params).forEach(([key, value]) => { if (value !== undefined && value !== "") url.searchParams.set(key, value); }); } return url.toString(); } async function readErrorDetail(response: Response): Promise { const text = await response.text(); try { const parsed = JSON.parse(text) as { detail?: unknown; message?: unknown }; const detail = parsed.detail ?? parsed.message; if (typeof detail === "string" && detail.trim()) { return detail; } } catch { // Fall back to the raw response body below. } return text; } function buildHeaders(isJsonBody: boolean): Headers { const headers = new Headers(); const token = getAccessToken(); if (token) headers.set("Authorization", `Bearer ${token}`); if (isJsonBody) headers.set("Content-Type", "application/json"); return headers; } async function get( path: string, params?: Record, ): Promise { const response = await fetch(buildUrl(path, params), { headers: buildHeaders(false), }); if (!response.ok) { throw new Error(`${response.status}: ${await readErrorDetail(response)}`); } return response.json(); } async function post(path: string, body?: unknown): Promise { const response = await fetch(buildUrl(path), { method: "POST", headers: buildHeaders(true), body: body ? JSON.stringify(body) : undefined, }); if (!response.ok) { throw new Error(`${response.status}: ${await readErrorDetail(response)}`); } return response.json(); } async function postForm(path: string, body: FormData): Promise { const headers = buildHeaders(false); const response = await fetch(buildUrl(path), { method: "POST", headers, body, }); if (!response.ok) { throw new Error(`${response.status}: ${await readErrorDetail(response)}`); } return response.json(); } async function del(path: string): Promise { const response = await fetch(buildUrl(path), { method: "DELETE", headers: buildHeaders(false), }); if (!response.ok) { throw new Error(`${response.status}: ${await readErrorDetail(response)}`); } return response.json(); } // Dashboard export const fetchCounts = (machineId?: string) => get( "/api/dashboard/counts", machineId ? { machine_id: machineId } : undefined, ); export const fetchLibraries = (machineId?: string) => get( "/api/dashboard/libraries", machineId ? { machine_id: machineId } : undefined, ); export const fetchActivity = (machineId?: string) => get( "/api/dashboard/activity", machineId ? { machine_id: machineId } : undefined, ); export const fetchUsers = (machineId?: string) => get( "/api/users", machineId ? { machine_id: machineId } : undefined, ); // Backward-compatible alias used by older hooks/components. export const fetchNowPlaying = fetchActivity; // Monitoring export const fetchMonitoringMachines = () => get("/api/monitoring/machines"); export const fetchMonitoringPoller = () => get("/api/monitoring/poller"); export const fetchAppVersion = () => get("/api/version"); export const fetchMonitoringOverview = () => get("/api/dashboard/monitoring"); export const fetchDashboardShortcuts = () => get("/api/dashboard/shortcuts"); export const saveDashboardShortcut = (shortcut: DashboardShortcutInput) => fetch( buildUrl( shortcut.id ? `/api/dashboard/shortcuts/${encodeURIComponent(shortcut.id)}` : "/api/dashboard/shortcuts", ), { method: shortcut.id ? "PUT" : "POST", headers: buildHeaders(true), body: JSON.stringify(shortcut), }, ).then(async (response) => { if (!response.ok) { throw new Error(`${response.status}: ${await readErrorDetail(response)}`); } return response.json() as Promise; }); export const deleteDashboardShortcut = (shortcutId: string) => del<{ status: string }>( `/api/dashboard/shortcuts/${encodeURIComponent(shortcutId)}`, ); export const fetchMonitoringStatus = (machineId?: string) => get( "/api/monitoring/status", machineId ? { machine_id: machineId } : undefined, ); export const fetchMonitoringMetrics = ( lastSeconds?: number | null, maxLines = 70_000, machineId?: string, ) => get("/api/monitoring/metrics", { ...(lastSeconds == null ? {} : { last_seconds: String(lastSeconds) }), max_lines: String(maxLines), ...(machineId ? { machine_id: machineId } : {}), }); export const fetchDiskSpace = (machineId?: string) => get( "/api/monitoring/disk", machineId ? { machine_id: machineId } : undefined, ); export const startCollector = (machineId?: string) => post<{ message: string }>( machineId ? `/api/monitoring/start?machine_id=${encodeURIComponent(machineId)}` : "/api/monitoring/start", ); export const stopCollector = (machineId?: string) => post<{ message: string }>( machineId ? `/api/monitoring/stop?machine_id=${encodeURIComponent(machineId)}` : "/api/monitoring/stop", ); export const restartCollector = (machineId?: string) => post<{ message: string }>( machineId ? `/api/monitoring/restart?machine_id=${encodeURIComponent(machineId)}` : "/api/monitoring/restart", ); export const fetchMonitoringSettings = () => get("/api/settings/machines"); export const fetchSSHKeys = () => get("/api/settings/ssh-keys"); export const generateSSHKey = (payload: { name: string; passphrase: string; notes: string; bits?: number; }) => post("/api/settings/ssh-keys/generate", payload); export const fetchMonitoringMachineActions = (machineId: string, limit = 10) => get<{ items: MonitoringMachineAction[]; total: number }>( `/api/monitoring/machines/${encodeURIComponent(machineId)}/actions`, { limit: String(limit) }, ); export const saveSSHKey = (key: SSHKeyInput) => fetch( buildUrl( key.id ? `/api/settings/ssh-keys/${encodeURIComponent(key.id)}` : "/api/settings/ssh-keys", ), { method: key.id ? "PUT" : "POST", headers: buildHeaders(true), body: JSON.stringify(key), }, ).then(async (response) => { if (!response.ok) { throw new Error(`${response.status}: ${await readErrorDetail(response)}`); } return response.json() as Promise; }); export const deleteSSHKey = (keyId: string) => del<{ status: string }>( `/api/settings/ssh-keys/${encodeURIComponent(keyId)}`, ); export const fetchSavedTasks = () => get("/api/tasks"); export const fetchSavedTaskRuns = (taskId: string, limit = 10) => get<{ items: SavedTaskRun[]; total: number }>( `/api/tasks/${encodeURIComponent(taskId)}/runs`, { limit: String(limit), }, ); export const saveTask = (task: SavedTaskInput) => fetch( buildUrl( task.id ? `/api/tasks/${encodeURIComponent(task.id)}` : "/api/tasks", ), { method: task.id ? "PUT" : "POST", headers: buildHeaders(true), body: JSON.stringify(task), }, ).then(async (response) => { if (!response.ok) { throw new Error(`${response.status}: ${await readErrorDetail(response)}`); } return response.json() as Promise; }); export const deleteTask = (taskId: string) => del<{ status: string }>(`/api/tasks/${encodeURIComponent(taskId)}`); export const runTask = (taskId: string, machineId?: string) => post<{ task_id: string; task_name: string; machine_id: string; machine_name: string; task_type: string; exit_status: number; stdout: string; stderr: string; }>( machineId ? `/api/tasks/run?machine_id=${encodeURIComponent(machineId)}` : "/api/tasks/run", { task_id: taskId }, ); export const saveMonitoringMachine = (machine: MonitoringMachineInput) => fetch( buildUrl( machine.id ? `/api/settings/machines/${encodeURIComponent(machine.id)}` : "/api/settings/machines", ), { method: machine.id ? "PUT" : "POST", headers: buildHeaders(true), body: JSON.stringify(machine), }, ).then(async (response) => { if (!response.ok) { throw new Error(`${response.status}: ${await readErrorDetail(response)}`); } return response.json() as Promise; }); export const testMonitoringMachineSSH = (machine: MonitoringMachineInput) => post("/api/settings/machines/test-ssh", machine); export const deleteMonitoringMachine = (machineId: string) => del<{ status: string }>( `/api/settings/machines/${encodeURIComponent(machineId)}`, ); export const resetLocalDatabase = (payload: ResetLocalDatabaseInput) => post( "/api/settings/reset-local-database", payload, ); // Media export const fetchMediaStatus = (machineId?: string) => get( "/api/media/status", machineId ? { machine_id: machineId } : undefined, ); export const buildMediaIndex = (machineId?: string) => post( machineId ? `/api/media/build?machine_id=${encodeURIComponent(machineId)}` : "/api/media/build", ); export const stopMediaIndexBuild = (machineId?: string) => post( machineId ? `/api/media/stop?machine_id=${encodeURIComponent(machineId)}` : "/api/media/stop", ); export const forceStopMediaIndexBuild = (machineId?: string) => post( machineId ? `/api/media/force-stop?machine_id=${encodeURIComponent(machineId)}` : "/api/media/force-stop", ); export const queryMedia = (params: { libraries?: string; types?: string; search?: string; hdr_filter?: string; sort_key?: string; sort_order?: string; limit?: number; offset?: number; machineId?: string; }) => get("/api/media/query", { libraries: params.libraries || "", types: params.types || "Movie,Episode", search: params.search || "", hdr_filter: params.hdr_filter || "All", sort_key: params.sort_key || "title", sort_order: params.sort_order || "Ascending", limit: String(params.limit || 100), offset: String(params.offset || 0), ...(params.machineId ? { machine_id: params.machineId } : {}), }); // Files export const fetchDirectoryListing = (path: string, machineId?: string) => get("/api/files/list", { path, ...(machineId ? { machine_id: machineId } : {}), }); export const fetchFfprobe = (path: string, machineId?: string) => get>("/api/files/ffprobe", { path, ...(machineId ? { machine_id: machineId } : {}), }); export const fetchStat = (path: string, machineId?: string) => get<{ path: string; output: string }>("/api/files/stat", { path, ...(machineId ? { machine_id: machineId } : {}), }); export const resolvePath = (path: string, machineId?: string) => get("/api/files/resolve-path", { path, ...(machineId ? { machine_id: machineId } : {}), }); // Jobs export const fetchJobTemplates = () => get("/api/jobs/templates"); export const runJob = (jobKey: string, path: string, machineId?: string) => post( machineId ? `/api/jobs/run?machine_id=${encodeURIComponent(machineId)}` : "/api/jobs/run", { job_key: jobKey, path }, ); export const fetchUserMessageQueueStatus = () => get("/api/users/message/status"); export const sendUserMessage = (formData: FormData) => postForm("/api/users/message", formData);