import type { TokenResponse, UserResponse, ConfigResponse, ChannelResponse, CreateChannelRequest, UpdateChannelRequest, ScheduleResponse, ScheduledSlotResponse, CurrentBroadcastResponse, CollectionResponse, SeriesResponse, LibraryItemResponse, MediaFilter, TranscodeSettings, TranscodeStats, ActivityEvent, ProviderConfig, ProviderTestResult, ConfigSnapshot, ScheduleHistoryEntry, } from "@/lib/types"; const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000/api/v1"; export class ApiRequestError extends Error { constructor( public readonly status: number, message: string, ) { super(message); this.name = "ApiRequestError"; } } async function request( path: string, options: RequestInit & { token?: string } = {}, ): Promise { const { token, ...init } = options; const headers = new Headers(init.headers); if (token) { headers.set("Authorization", `Bearer ${token}`); } if (init.body && !headers.has("Content-Type")) { headers.set("Content-Type", "application/json"); } const res = await fetch(`${API_BASE}${path}`, { ...init, headers }); if (!res.ok) { let message = res.statusText; try { const body = await res.json(); message = body.message ?? body.error ?? message; } catch { // ignore parse error, use statusText } throw new ApiRequestError(res.status, message); } if (res.status === 204) return null as T; return res.json() as Promise; } export const api = { config: { get: () => request("/config"), }, auth: { register: (email: string, password: string) => request("/auth/register", { method: "POST", body: JSON.stringify({ email, password }), }), login: (email: string, password: string) => request("/auth/login", { method: "POST", body: JSON.stringify({ email, password }), }), logout: (token: string) => request("/auth/logout", { method: "POST", token }), me: (token: string) => request("/auth/me", { token }), }, channels: { list: (token: string) => request("/channels", { token }), get: (id: string, token: string) => request(`/channels/${id}`, { token }), create: (data: CreateChannelRequest, token: string) => request("/channels", { method: "POST", body: JSON.stringify(data), token, }), update: (id: string, data: UpdateChannelRequest, token: string) => request(`/channels/${id}`, { method: "PUT", body: JSON.stringify(data), token, }), delete: (id: string, token: string) => request(`/channels/${id}`, { method: "DELETE", token }), listConfigHistory: (channelId: string, token: string) => request(`/channels/${channelId}/config/history`, { token }), patchConfigSnapshot: (channelId: string, snapId: string, label: string | null, token: string) => request(`/channels/${channelId}/config/history/${snapId}`, { method: "PATCH", body: JSON.stringify({ label }), token, }), restoreConfigSnapshot: (channelId: string, snapId: string, token: string) => request(`/channels/${channelId}/config/history/${snapId}/restore`, { method: "POST", token, }), listScheduleHistory: (channelId: string, token: string) => request(`/channels/${channelId}/schedule/history`, { token }), getScheduleGeneration: (channelId: string, genId: string, token: string) => request(`/channels/${channelId}/schedule/history/${genId}`, { token }), rollbackSchedule: (channelId: string, genId: string, token: string) => request(`/channels/${channelId}/schedule/history/${genId}/rollback`, { method: "POST", token, }), }, library: { collections: (token: string, provider?: string) => { const params = new URLSearchParams(); if (provider) params.set("provider", provider); const qs = params.toString(); return request(`/library/collections${qs ? `?${qs}` : ""}`, { token }); }, series: (token: string, collectionId?: string, provider?: string) => { const params = new URLSearchParams(); if (collectionId) params.set("collection", collectionId); if (provider) params.set("provider", provider); const qs = params.toString(); return request(`/library/series${qs ? `?${qs}` : ""}`, { token }); }, genres: (token: string, contentType?: string, provider?: string) => { const params = new URLSearchParams(); if (contentType) params.set("type", contentType); if (provider) params.set("provider", provider); const qs = params.toString(); return request(`/library/genres${qs ? `?${qs}` : ""}`, { token }); }, items: ( token: string, filter: Pick, limit = 50, strategy?: string, provider?: string, ) => { const params = new URLSearchParams(); if (filter.search_term) params.set("q", filter.search_term); if (filter.content_type) params.set("type", filter.content_type); filter.series_names?.forEach((name) => params.append("series[]", name)); if (filter.collections?.[0]) params.set("collection", filter.collections[0]); params.set("limit", String(limit)); if (strategy) params.set("strategy", strategy); if (provider) params.set("provider", provider); return request(`/library/items?${params}`, { token }); }, }, files: { rescan: (token: string) => request<{ items_found: number }>("/files/rescan", { method: "POST", token }), }, transcode: { getSettings: (token: string) => request("/files/transcode-settings", { token }), updateSettings: (data: TranscodeSettings, token: string) => request("/files/transcode-settings", { method: "PUT", body: JSON.stringify(data), token, }), getStats: (token: string) => request("/files/transcode-stats", { token }), clearCache: (token: string) => request("/files/transcode-cache", { method: "DELETE", token }), }, admin: { activity: (token: string) => request("/admin/activity", { token }), providers: { getProviders: (token: string) => request("/admin/providers", { token }), updateProvider: ( token: string, type: string, payload: { config_json: Record; enabled: boolean }, ) => request(`/admin/providers/${type}`, { method: "PUT", body: JSON.stringify(payload), token, }), deleteProvider: (token: string, type: string) => request(`/admin/providers/${type}`, { method: "DELETE", token }), testProvider: ( token: string, type: string, payload: { config_json: Record; enabled: boolean }, ) => request(`/admin/providers/${type}/test`, { method: "POST", body: JSON.stringify(payload), token, }), }, }, schedule: { generate: (channelId: string, token: string) => request(`/channels/${channelId}/schedule`, { method: "POST", token, }), getActive: (channelId: string, token: string) => request(`/channels/${channelId}/schedule`, { token }), getCurrentBroadcast: (channelId: string, token: string, channelPassword?: string) => { const headers: Record = {}; if (channelPassword) headers["X-Channel-Password"] = channelPassword; return request(`/channels/${channelId}/now`, { token, headers, }); }, getEpg: ( channelId: string, token: string, from?: string, until?: string, channelPassword?: string, ) => { const params = new URLSearchParams(); if (from) params.set("from", from); if (until) params.set("until", until); const qs = params.toString(); const headers: Record = {}; if (channelPassword) headers["X-Channel-Password"] = channelPassword; return request( `/channels/${channelId}/epg${qs ? `?${qs}` : ""}`, { token, headers }, ); }, }, };