import type { Me, ServerStateResponse, Track } from "./types"; export class ApiError extends Error {} async function request(path: string, init: RequestInit = {}): Promise { const res = await fetch(path, { credentials: "same-origin", headers: init.body ? { "content-type": "application/json" } : undefined, ...init, }); const text = await res.text(); const body = text ? JSON.parse(text) : null; if (!res.ok) throw new ApiError(body?.error ?? `Ошибка ${res.status}`); return body as T; } export interface LoginResponse { user?: { id: string; username: string }; mfaRequired?: boolean; ticket?: string; methods?: string[]; } export const api = { me: () => request("/api/me"), login: (payload: { email?: string; password?: string; mfaTicket?: string; totpCode?: string; recoveryCode?: string; }) => request("/api/auth/login", { method: "POST", body: JSON.stringify(payload) }), loginWithLink: (token: string) => request<{ user: { id: string; username: string }; serverId: string | null }>("/api/auth/link", { method: "POST", body: JSON.stringify({ token }), }), logout: () => request<{ ok: true }>("/api/auth/logout", { method: "POST" }), state: (serverId: string) => request(`/api/servers/${serverId}/state`), search: (serverId: string, query: string) => request<{ tracks: Track[] }>(`/api/servers/${serverId}/search?q=${encodeURIComponent(query)}`), play: ( serverId: string, payload: { query?: string; trackIds?: string[]; mode?: "append" | "next" | "now"; voiceChannelId?: string | null }, ) => request<{ ok: true }>(`/api/servers/${serverId}/play`, { method: "POST", body: JSON.stringify(payload) }), action: (serverId: string, action: string, payload: Record = {}) => request<{ ok: true }>(`/api/servers/${serverId}/actions/${action}`, { method: "POST", body: JSON.stringify(payload), }), join: (serverId: string, voiceChannelId: string | null) => request<{ ok: true }>(`/api/servers/${serverId}/join`, { method: "POST", body: JSON.stringify({ voiceChannelId }), }), removeTrack: (serverId: string, trackId: string) => request<{ ok: true }>(`/api/servers/${serverId}/queue/${trackId}`, { method: "DELETE" }), moveTrack: (serverId: string, trackId: string, index: number) => request<{ ok: true }>(`/api/servers/${serverId}/queue/${trackId}/move`, { method: "POST", body: JSON.stringify({ index }), }), }; /** Clock formatting for any position or length; 0 is a legitimate "0:00". */ export function formatDuration(seconds: number): string { if (!Number.isFinite(seconds) || seconds < 0) return "0:00"; const total = Math.floor(seconds); const hours = Math.floor(total / 3600); const minutes = Math.floor((total % 3600) / 60); const secs = total % 60; const pad = (value: number) => value.toString().padStart(2, "0"); return hours > 0 ? `${hours}:${pad(minutes)}:${pad(secs)}` : `${minutes}:${pad(secs)}`; } /** Track length as shown to people: live streams and unknown lengths are not clocks. */ export function formatLength(track: { duration: number; isLive: boolean }): string { if (track.isLive) return "LIVE"; if (track.duration <= 0) return "—"; return formatDuration(track.duration); }