Files
manage/frontend/src/api/client.ts
T

431 lines
12 KiB
TypeScript

/**
* 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, string>): 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<string> {
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<T>(
path: string,
params?: Record<string, string>,
): Promise<T> {
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<T>(path: string, body?: unknown): Promise<T> {
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<T>(path: string, body: FormData): Promise<T> {
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<T>(path: string): Promise<T> {
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<MediaCounts>(
"/api/dashboard/counts",
machineId ? { machine_id: machineId } : undefined,
);
export const fetchLibraries = (machineId?: string) =>
get<LibraryCount[]>(
"/api/dashboard/libraries",
machineId ? { machine_id: machineId } : undefined,
);
export const fetchActivity = (machineId?: string) =>
get<NowPlayingSession[]>(
"/api/dashboard/activity",
machineId ? { machine_id: machineId } : undefined,
);
export const fetchUsers = (machineId?: string) =>
get<UserDirectoryResponse>(
"/api/users",
machineId ? { machine_id: machineId } : undefined,
);
// Backward-compatible alias used by older hooks/components.
export const fetchNowPlaying = fetchActivity;
// Monitoring
export const fetchMonitoringMachines = () =>
get<MonitoringMachine[]>("/api/monitoring/machines");
export const fetchMonitoringPoller = () =>
get<MonitoringPollerStatus>("/api/monitoring/poller");
export const fetchAppVersion = () => get<AppVersionInfo>("/api/version");
export const fetchMonitoringOverview = () =>
get<MonitoringOverviewResponse>("/api/dashboard/monitoring");
export const fetchDashboardShortcuts = () =>
get<DashboardShortcut[]>("/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<DashboardShortcut>;
});
export const deleteDashboardShortcut = (shortcutId: string) =>
del<{ status: string }>(
`/api/dashboard/shortcuts/${encodeURIComponent(shortcutId)}`,
);
export const fetchMonitoringStatus = (machineId?: string) =>
get<MonitoringStatus>(
"/api/monitoring/status",
machineId ? { machine_id: machineId } : undefined,
);
export const fetchMonitoringMetrics = (
lastSeconds?: number | null,
maxLines = 70_000,
machineId?: string,
) =>
get<MonitoringMetrics>("/api/monitoring/metrics", {
...(lastSeconds == null ? {} : { last_seconds: String(lastSeconds) }),
max_lines: String(maxLines),
...(machineId ? { machine_id: machineId } : {}),
});
export const fetchDiskSpace = (machineId?: string) =>
get<DiskSpace>(
"/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<MonitoringMachine[]>("/api/settings/machines");
export const fetchSSHKeys = () => get<SSHKey[]>("/api/settings/ssh-keys");
export const generateSSHKey = (payload: {
name: string;
passphrase: string;
notes: string;
bits?: number;
}) => post<SSHKeyGenerated>("/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<SSHKey>;
});
export const deleteSSHKey = (keyId: string) =>
del<{ status: string }>(
`/api/settings/ssh-keys/${encodeURIComponent(keyId)}`,
);
export const fetchSavedTasks = () => get<SavedTask[]>("/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<SavedTask>;
});
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<MonitoringMachine>;
});
export const testMonitoringMachineSSH = (machine: MonitoringMachineInput) =>
post<SSHValidationResult>("/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<ResetLocalDatabaseResponse>(
"/api/settings/reset-local-database",
payload,
);
// Media
export const fetchMediaStatus = (machineId?: string) =>
get<MediaIndexStatus>(
"/api/media/status",
machineId ? { machine_id: machineId } : undefined,
);
export const buildMediaIndex = (machineId?: string) =>
post<MediaIndexActionResponse>(
machineId
? `/api/media/build?machine_id=${encodeURIComponent(machineId)}`
: "/api/media/build",
);
export const stopMediaIndexBuild = (machineId?: string) =>
post<MediaIndexActionResponse>(
machineId
? `/api/media/stop?machine_id=${encodeURIComponent(machineId)}`
: "/api/media/stop",
);
export const forceStopMediaIndexBuild = (machineId?: string) =>
post<MediaIndexActionResponse>(
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<MediaQueryResponse>("/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<DirectoryListing>("/api/files/list", {
path,
...(machineId ? { machine_id: machineId } : {}),
});
export const fetchFfprobe = (path: string, machineId?: string) =>
get<Record<string, unknown>>("/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<ResolvedPath>("/api/files/resolve-path", {
path,
...(machineId ? { machine_id: machineId } : {}),
});
// Jobs
export const fetchJobTemplates = () =>
get<JobTemplate[]>("/api/jobs/templates");
export const runJob = (jobKey: string, path: string, machineId?: string) =>
post<JobResult>(
machineId
? `/api/jobs/run?machine_id=${encodeURIComponent(machineId)}`
: "/api/jobs/run",
{ job_key: jobKey, path },
);
export const fetchUserMessageQueueStatus = () =>
get<UserMessageQueueStatus>("/api/users/message/status");
export const sendUserMessage = (formData: FormData) =>
postForm<UserMessageResponse>("/api/users/message", formData);