/** * Typed API client for the FastAPI backend. */ import { getAccessToken } from "../auth"; import type { MediaCounts, LibraryCount, UserDirectoryResponse, UserMessageResponse, UserMessageQueueStatus, NowPlayingSession, MonitoringPollerStatus, MonitoringOverviewResponse, MonitoringStatus, MonitoringMetrics, DiskSpace, MonitoringMachine, MonitoringMachineInput, MonitoringMachineAction, MediaIndexStatus, MediaIndexActionResponse, MediaQueryResponse, DirectoryListing, JobTemplate, JobResult, ResolvedPath, } 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 = () => get("/api/dashboard/counts"); export const fetchLibraries = () => get("/api/dashboard/libraries"); export const fetchActivity = () => get("/api/dashboard/activity"); export const fetchUsers = () => get("/api/users"); // 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 fetchMonitoringOverview = () => get("/api/dashboard/monitoring"); 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 fetchMonitoringMachineActions = (machineId: string, limit = 10) => get<{ items: MonitoringMachineAction[]; total: number }>( `/api/monitoring/machines/${encodeURIComponent(machineId)}/actions`, { limit: String(limit) }, ); 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 deleteMonitoringMachine = (machineId: string) => del<{ status: string }>( `/api/settings/machines/${encodeURIComponent(machineId)}`, ); // Media export const fetchMediaStatus = () => get("/api/media/status"); export const buildMediaIndex = () => post("/api/media/build"); export const stopMediaIndexBuild = () => post("/api/media/stop"); export const forceStopMediaIndexBuild = () => post("/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; }) => 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), }); // Files export const fetchDirectoryListing = (path: string) => get("/api/files/list", { path }); export const fetchFfprobe = (path: string) => get>("/api/files/ffprobe", { path }); export const fetchStat = (path: string) => get<{ path: string; output: string }>("/api/files/stat", { path }); export const resolvePath = (path: string) => get("/api/files/resolve-path", { path }); // Jobs export const fetchJobTemplates = () => get("/api/jobs/templates"); export const runJob = (jobKey: string, path: string) => post("/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);