import { accounts } from "#lib/stores/accounts.svelte.js"; import { auth } from "#lib/stores/auth.svelte.js"; import { PUBLIC_API_BASE_URL } from "$app/env/public"; import { goto } from "$app/navigation"; const BASE = PUBLIC_API_BASE_URL; const MAX_LIMIT = 500; export class ApiError extends Error { status: number; detail: string; constructor(status: number, detail: string) { super(detail); this.name = "ApiError"; this.status = status; this.detail = detail; } } type QueryValue = string | number | boolean | null | undefined; interface RequestOptions { account?: boolean; body?: unknown; method?: string; query?: Record; } function buildQuery( query: Record | undefined, withAccount: boolean ): string { const params = new URLSearchParams(); if (withAccount && accounts.selectedId !== null) { params.set("account_id", String(accounts.selectedId)); } if (query) { for (const [key, value] of Object.entries(query)) { if (value === null || value === undefined) { continue; } const clamped = key === "limit" && typeof value === "number" ? Math.min(value, MAX_LIMIT) : value; params.set(key, String(clamped)); } } const text = params.toString(); return text ? `?${text}` : ""; } function authHeaders(): Record { return auth.token ? { Authorization: `Bearer ${auth.token}` } : {}; } async function handleError(response: Response): Promise { if (response.status === 401) { auth.logout(); await goto("/login"); } let detail = response.statusText; try { const data = await response.json(); if (data && typeof data.detail === "string") { ({ detail } = data); } } catch { detail = response.statusText; } throw new ApiError(response.status, detail); } export async function request( path: string, options: RequestOptions = {} ): Promise { const { method = "GET", query, body, account = false } = options; const headers: Record = authHeaders(); if (body !== undefined) { headers["Content-Type"] = "application/json"; } const response = await fetch(`${BASE}${path}${buildQuery(query, account)}`, { body: body === undefined ? undefined : JSON.stringify(body), headers, method, }); if (!response.ok) { return handleError(response); } if (response.status === 204) { return undefined as T; } return response.json() as Promise; } function streamUrl(path: string): string { const query = auth.token ? `?token=${encodeURIComponent(auth.token)}` : ""; return `${BASE}${path}${query}`; } export function mediaUrl(mediaId: number): string { return streamUrl(`/media/${mediaId}`); } export function trashedUrl(storageKey: string): string { return streamUrl(`/storage/trash/${storageKey}/file`); } export type MediaResult = | { state: "ready"; url: string; mime: string | null } | { state: "not-downloaded" } | { state: "missing" }; export async function requestMedia(mediaId: number): Promise { const response = await fetch(`${BASE}/media/${mediaId}`, { headers: authHeaders(), }); if (response.status === 409) { return { state: "not-downloaded" }; } if (response.status === 404) { return { state: "missing" }; } if (!response.ok) { return handleError(response); } const blob = await response.blob(); return { mime: response.headers.get("Content-Type"), state: "ready", url: URL.createObjectURL(blob), }; } export async function requestMediaVersion( versionId: number ): Promise { const response = await fetch(`${BASE}/media/version/${versionId}`, { headers: authHeaders(), }); if (response.status === 404) { return { state: "missing" }; } if (!response.ok) { return handleError(response); } const blob = await response.blob(); return { mime: response.headers.get("Content-Type"), state: "ready", url: URL.createObjectURL(blob), }; }