/** * Typed API client for the FastAPI backend. */ import { getAccessToken } from "../auth"; import type { MediaCounts, LibraryCount, UserDirectoryResponse, UserMessageResponse, UserMessageQueueStatus, NowPlayingSession, MonitoringStatus, MonitoringMetrics, DiskSpace, 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(); } // 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 fetchMonitoringStatus = () => get("/api/monitoring/status"); export const fetchMonitoringMetrics = ( lastSeconds?: number | null, maxLines = 70_000, ) => get("/api/monitoring/metrics", { ...(lastSeconds == null ? {} : { last_seconds: String(lastSeconds) }), max_lines: String(maxLines), }); export const fetchDiskSpace = () => get("/api/monitoring/disk"); export const startCollector = () => post<{ message: string }>("/api/monitoring/start"); export const stopCollector = () => post<{ message: string }>("/api/monitoring/stop"); export const restartCollector = () => post<{ message: string }>("/api/monitoring/restart"); // 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);