137 lines
4.1 KiB
TypeScript
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`;
|
|
}
|