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,41 @@
import { fetchWithTimeout } from '../base.js';
export interface MetubeQueueItem {
readonly id: string;
readonly title: string;
readonly status: string;
readonly percent: number;
readonly msg?: string;
readonly filename?: string;
}
export interface MetubeQueueResponse {
readonly done: Record<string, MetubeQueueItem>;
readonly queue: Record<string, MetubeQueueItem>;
}
export interface MetubeHistoryResponse {
readonly done: Record<string, MetubeQueueItem>;
}
function buildUrl(appUrl: string, path: string): string {
return `${appUrl.replace(/\/$/, '')}/api/${path}`;
}
export async function fetchQueue(appUrl: string): Promise<MetubeQueueResponse> {
const url = buildUrl(appUrl, 'queue');
const res = await fetchWithTimeout(url);
if (!res.ok) {
throw new Error(`MeTube API returned ${res.status}`);
}
return res.json();
}
export async function fetchHistory(appUrl: string): Promise<MetubeHistoryResponse> {
const url = buildUrl(appUrl, 'history');
const res = await fetchWithTimeout(url);
if (!res.ok) {
throw new Error(`MeTube API returned ${res.status}`);
}
return res.json();
}
@@ -0,0 +1,66 @@
import type { Integration, IntegrationData, IntegrationEndpoint, IntegrationTestResult } from '../types.js';
import { wrapError } from '../base.js';
import { metubeAuthConfigSchema } from './schema.js';
import { fetchQueue } from './client.js';
import { toDownloadQueue } from './transform.js';
const endpoints: readonly IntegrationEndpoint[] = [
{
id: 'download-queue',
name: 'Download Queue',
description: 'Active and pending downloads with progress',
renderer: 'progress',
refreshInterval: 10
}
] as const;
async function testConnection(
appUrl: string,
_config: Record<string, unknown>
): Promise<IntegrationTestResult> {
try {
metubeAuthConfigSchema.parse(_config);
await fetchQueue(appUrl);
return { success: true, message: 'Connected to MeTube' };
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return { success: false, message: `Failed to connect to MeTube: ${message}` };
}
}
async function fetchData(
appUrl: string,
config: Record<string, unknown>,
endpointId: string
): Promise<IntegrationData> {
metubeAuthConfigSchema.parse(config);
try {
switch (endpointId) {
case 'download-queue': {
const response = await fetchQueue(appUrl);
return {
endpointId,
renderer: 'progress',
data: toDownloadQueue(response.queue),
fetchedAt: new Date().toISOString()
};
}
default:
throw new Error(`Unknown endpoint: ${endpointId}`);
}
} catch (error) {
throw wrapError('metube', endpointId, error);
}
}
export const metubeIntegration: Integration = {
id: 'metube',
name: 'MeTube',
icon: 'metube',
description: 'YouTube video downloader with queue progress',
authConfigSchema: metubeAuthConfigSchema,
endpoints,
testConnection,
fetchData
};
@@ -0,0 +1,5 @@
import { z } from 'zod';
export const metubeAuthConfigSchema = z.object({});
export type MetubeAuthConfig = z.infer<typeof metubeAuthConfigSchema>;
@@ -0,0 +1,17 @@
import type { ProgressData } from '../types.js';
import type { MetubeQueueItem } from './client.js';
export function toDownloadQueue(
queue: Record<string, MetubeQueueItem>
): ProgressData {
const entries = Object.entries(queue);
return {
items: entries.map(([id, item]) => ({
id,
label: item.title || item.filename || id,
progress: Math.round(item.percent ?? 0),
subtitle: item.status || undefined
}))
};
}