torrent/ui/src/lib/api.ts

137 lines
4.1 KiB
TypeScript

const BASE = "/api";
export interface TorrentState {
infoHashHex: string;
name: string;
trackerUrl: string;
uploaded: number;
downloaded: number;
left: number;
status: "idle" | "running" | "paused" | "stopping" | "stopped" | "error";
lastAnnounce: string | null;
lastInterval: number;
seeders: number;
leechers: number;
peers: number;
error: string | null;
addedAt: string;
}
export interface GlobalStats {
activeSeeders: number;
totalTorrents: number;
totalUploaded: number;
uptimeSeconds: number;
startedAt: string;
}
/** Extract a human-readable error message from a failed response */
async function extractError(res: Response): Promise<string> {
try {
const body = await res.json();
if (body && typeof body === "object" && "error" in body) {
return typeof body.error === "string" ? body.error : JSON.stringify(body.error);
}
} catch {
// response wasn't JSON
}
return `Request failed (${res.status})`;
}
export async function listTorrents(): Promise<TorrentState[]> {
const res = await fetch(`${BASE}/torrents`);
if (!res.ok) throw new Error(await extractError(res));
return res.json();
}
export async function addTorrent(file: File): Promise<TorrentState> {
const form = new FormData();
form.append("torrent", file);
const res = await fetch(`${BASE}/torrents`, { method: "POST", body: form });
if (!res.ok) throw new Error(await extractError(res));
return res.json();
}
export async function removeTorrent(hash: string): Promise<void> {
const res = await fetch(`${BASE}/torrents/${hash}`, { method: "DELETE" });
if (!res.ok) throw new Error(await extractError(res));
}
export async function pauseTorrent(hash: string): Promise<void> {
const res = await fetch(`${BASE}/torrents/${hash}/pause`, { method: "POST" });
if (!res.ok) throw new Error(await extractError(res));
}
export async function resumeTorrent(hash: string): Promise<void> {
const res = await fetch(`${BASE}/torrents/${hash}/resume`, { method: "POST" });
if (!res.ok) throw new Error(await extractError(res));
}
export interface AppConfig {
port: number;
announcePort: number;
minUploadRateBytesPerSec: number;
maxUploadRateBytesPerSec: number;
clientProfile: "qbittorrent" | "transmission";
torrentsDir: string;
autoLoad: boolean;
}
export async function getConfig(): Promise<AppConfig> {
const res = await fetch(`${BASE}/config`);
if (!res.ok) throw new Error(await extractError(res));
return res.json();
}
export async function updateConfig(patch: Partial<Pick<AppConfig, "minUploadRateBytesPerSec" | "maxUploadRateBytesPerSec" | "clientProfile" | "announcePort">>): Promise<AppConfig> {
const res = await fetch(`${BASE}/config`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(patch),
});
if (!res.ok) throw new Error(await extractError(res));
return res.json();
}
export async function getStats(): Promise<GlobalStats> {
const res = await fetch(`${BASE}/status`);
if (!res.ok) throw new Error(await extractError(res));
return res.json();
}
export function connectSSE(
onTorrent: (state: TorrentState) => void,
onStats: (stats: GlobalStats) => void,
onTorrents: (torrents: TorrentState[]) => void,
): EventSource {
const es = new EventSource(`${BASE}/status/stream`);
es.addEventListener("torrent", (e) => {
try { onTorrent(JSON.parse(e.data)); } catch { /* malformed SSE data */ }
});
es.addEventListener("stats", (e) => {
try { onStats(JSON.parse(e.data)); } catch { /* malformed SSE data */ }
});
es.addEventListener("torrents", (e) => {
try { onTorrents(JSON.parse(e.data)); } catch { /* malformed SSE data */ }
});
return es;
}
export function formatBytes(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 ** 2) return `${(bytes / 1024).toFixed(1)} KB`;
if (bytes < 1024 ** 3) return `${(bytes / 1024 ** 2).toFixed(1)} MB`;
return `${(bytes / 1024 ** 3).toFixed(2)} GB`;
}
export function formatUptime(seconds: number): string {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = seconds % 60;
return `${h}h ${m}m ${s}s`;
}