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:
@@ -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
|
||||
}))
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user