feat(service-integrations): phases 9-10 — media integrations + Planka

- Emby: now playing, library stats, recently added, active streams
- Immich: library stats, recent uploads with formatted storage
- Deluge: active torrents with progress, transfer speed, disk space gauge
- MeTube: download queue progress (no auth required)
- Planka: my cards, overdue cards with red badges, board summary
- All 11 integrations registered in registry
This commit is contained in:
2026-03-25 22:16:27 +03:00
parent d73fb9c680
commit 55e220bc07
21 changed files with 1227 additions and 0 deletions
@@ -0,0 +1,103 @@
import { fetchWithTimeout } from '../base.js';
export interface DelugeTorrent {
readonly name: string;
readonly progress: number;
readonly state: string;
readonly download_payload_rate: number;
readonly upload_payload_rate: number;
readonly eta: number;
readonly total_size: number;
}
export interface DelugeUIResponse {
readonly result: {
readonly torrents: Record<string, DelugeTorrent>;
readonly stats: {
readonly download_rate: number;
readonly upload_rate: number;
};
};
}
export interface DelugeFreeSpaceResponse {
readonly result: number;
}
function rpcUrl(appUrl: string): string {
return `${appUrl.replace(/\/$/, '')}/json`;
}
async function rpcCall(
url: string,
method: string,
params: unknown[],
id: number,
cookie?: string
): Promise<Response> {
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (cookie) {
headers['Cookie'] = cookie;
}
const res = await fetchWithTimeout(url, {
method: 'POST',
headers,
body: JSON.stringify({ method, params, id })
});
if (!res.ok) {
throw new Error(`Deluge API returned ${res.status}`);
}
return res;
}
function extractCookie(res: Response): string {
const setCookie = res.headers.get('set-cookie');
if (!setCookie) return '';
const match = setCookie.match(/^([^;]+)/);
return match ? match[1] : '';
}
export async function authenticate(appUrl: string, password: string): Promise<string> {
const url = rpcUrl(appUrl);
const res = await rpcCall(url, 'auth.login', [password], 1);
const body = await res.json();
if (!body.result) {
throw new Error('Deluge authentication failed — invalid password');
}
const cookie = extractCookie(res);
if (!cookie) {
throw new Error('Deluge did not return a session cookie');
}
return cookie;
}
export async function fetchUIData(
appUrl: string,
cookie: string
): Promise<DelugeUIResponse> {
const url = rpcUrl(appUrl);
const fields = [
'name',
'progress',
'state',
'download_payload_rate',
'upload_payload_rate',
'eta',
'total_size'
];
const res = await rpcCall(url, 'web.update_ui', [fields, {}], 2, cookie);
return res.json();
}
export async function fetchFreeSpace(
appUrl: string,
cookie: string,
path = '/'
): Promise<DelugeFreeSpaceResponse> {
const url = rpcUrl(appUrl);
const res = await rpcCall(url, 'core.get_free_space', [path], 3, cookie);
return res.json();
}