// Thin API client + SSE live stream. async function jget(url) { const r = await fetch(url); if (!r.ok) throw new Error(`${url} → ${r.status}`); return r.json(); } async function jpost(url, body) { const r = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body || {}), }); if (!r.ok) throw new Error(`${url} → ${r.status}`); return r.json(); } export const api = { authStatus: () => jget('/api/auth/status'), login: (username, password) => jpost('/api/login', { username, password }), logout: () => jpost('/api/logout', {}), // account management changePassword: (oldPassword, newPassword) => jpost('/api/account/password', { oldPassword, newPassword }), listUsers: () => jget('/api/users'), createUser: (username, password, role) => jpost('/api/users', { username, password, role }), deleteUser: (username) => jpost('/api/users/delete', { username }), setUserPassword: (username, password) => jpost('/api/users/password', { username, password }), setUserRole: (username, role) => jpost('/api/users/role', { username, role }), plugins: () => jget('/api/plugins'), meta: () => jget('/api/meta'), snapshot: () => jget('/api/snapshot'), torrent: (hash) => jget(`/api/torrents/${hash}`), trackers: (hash) => jget(`/api/torrents/${hash}/trackers`), peers: (hash) => jget(`/api/torrents/${hash}/peers`), files: (hash) => jget(`/api/torrents/${hash}/files`), pieces: (hash) => jget(`/api/torrents/${hash}/pieces`), action: (action, hashes, params) => jpost('/api/action', { action, hashes, params }), remove: (hashes, deleteFiles) => jpost('/api/delete', { hashes, deleteFiles }), add: (opts) => jpost('/api/add', opts), toggleAltSpeed: () => jpost('/api/altspeed', {}), createCategory: (name, savePath) => jpost('/api/categories', { name, savePath }), deleteCategory: (name) => jpost('/api/categories/delete', { name }), editCategory: (name, newName, savePath) => jpost('/api/categories/edit', { name, newName, savePath }), createTag: (name) => jpost('/api/tags', { name }), deleteTag: (name) => jpost('/api/tags/delete', { name }), rss: () => jget('/api/rss'), rssRules: () => jget('/api/rss/rules'), addFeed: (name, url) => jpost('/api/rss', { name, url }), deleteFeed: (name) => jpost('/api/rss/delete', { name }), saveRssRule: (rule) => jpost('/api/rss/rules', rule), deleteRssRule: (name) => jpost('/api/rss/rules/delete', { name }), runRssRule: (name) => jpost('/api/rss/rules/run', { name }), refreshFeeds: (name) => jpost('/api/rss/refresh', name ? { name } : {}), rssDownload: (item) => jpost('/api/rss/download', item), saveIndexer: (ix) => jpost('/api/indexers', ix), deleteIndexer: (name) => jpost('/api/indexers/delete', { name }), script: () => jget('/api/script'), saveScript: (source) => jpost('/api/script', { source }), saveScriptSettings: (settings) => jpost('/api/script/settings', { settings }), search: (q) => jget(`/api/search?q=${encodeURIComponent(q)}`), // Live snapshot stream. onSnapshot(snapshot) called ~1/s. stream(onSnapshot, onStatus) { const es = new EventSource('/api/stream'); es.addEventListener('snapshot', (e) => onSnapshot(JSON.parse(e.data))); es.onopen = () => onStatus && onStatus('connected'); es.onerror = () => onStatus && onStatus('reconnecting'); return es; }, };