156 lines
4.0 KiB
TypeScript
156 lines
4.0 KiB
TypeScript
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<string, QueryValue>;
|
|
}
|
|
|
|
function buildQuery(
|
|
query: Record<string, QueryValue> | 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<string, string> {
|
|
return auth.token ? { Authorization: `Bearer ${auth.token}` } : {};
|
|
}
|
|
|
|
async function handleError(response: Response): Promise<never> {
|
|
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<T>(
|
|
path: string,
|
|
options: RequestOptions = {}
|
|
): Promise<T> {
|
|
const { method = "GET", query, body, account = false } = options;
|
|
const headers: Record<string, string> = 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<T>;
|
|
}
|
|
|
|
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<MediaResult> {
|
|
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<MediaResult> {
|
|
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),
|
|
};
|
|
}
|