From 845022621115860a9e7424f6fb245308b97c66fd Mon Sep 17 00:00:00 2001 From: Alexandre Tuleu Date: Fri, 22 Sep 2023 10:34:51 +0200 Subject: [PATCH] Refactors services with defaultConfig and polled data. Much more simpler code base. Each service requires a poll functioon to poll its status. If type is defined, and the registered '+service.ts' exports a poll ServicePoller, it will be used. Otherwise, a generic poll mechanism is used. --- src/config.yml | 12 ++---- src/lib/components/ServiceCard.svelte | 30 +++++--------- src/lib/components/ServiceGroup.svelte | 7 ++-- src/lib/server/config.ts | 26 +++++++++++- src/lib/server/serviceDataPolling.ts | 55 +++---------------------- src/lib/services/jellyfin/+service.ts | 11 +++-- src/lib/services/pihole/+content.svelte | 20 +++++---- src/lib/services/pihole/+service.ts | 31 +++++++------- src/lib/services/prowlarr/+service.ts | 8 ++-- src/lib/services/radarr/+service.ts | 11 +++-- src/lib/services/service.ts | 7 ++-- src/lib/services/services.ts | 52 +++++++++++++++++------ src/lib/services/sonarr/+content.svelte | 4 +- src/lib/services/sonarr/+service.ts | 34 ++++++++------- static/config.yml | 14 ------- 15 files changed, 156 insertions(+), 166 deletions(-) delete mode 100644 static/config.yml diff --git a/src/config.yml b/src/config.yml index 5aa584b..9aa7691 100644 --- a/src/config.yml +++ b/src/config.yml @@ -18,20 +18,17 @@ services: - title: '/Media' icon: 'fas fa-photo-film' items: - - title: 'Jellyfin' + - type: jellyfin url: 'https://eagle.tuleu.me' - type: jellyfin keywords: 'cloud storage files' - - title: 'Sonarr' + - type: sonarr url: 'http://sonarr.lan' - type: sonarr api_key: 43f13770f9a0419bbdc3224dae76e886 keywords: 'shows tracker torrent usenet' - - title: 'Radarr' + - type: radarr url: 'http://radarr.lan' - type: radarr keywords: 'movies tracker torrent usenet' - title: '/Cloud' @@ -46,8 +43,7 @@ services: - title: '/Infra' icon: 'fas fa-network-wired' items: - - title: 'PiHole' + - type: pihole url: 'http://pihole.lan/admin' - type: 'pihole' keywords: 'dns ads blocker internet' api_token: a3996b80e3d9cdb86b338396a164a8814e8d6f44d2986261fe573bfea53a75fb diff --git a/src/lib/components/ServiceCard.svelte b/src/lib/components/ServiceCard.svelte index 5a84d19..4a192ed 100644 --- a/src/lib/components/ServiceCard.svelte +++ b/src/lib/components/ServiceCard.svelte @@ -1,27 +1,19 @@
- {#if data.data?.status == 'enabled'} + {#if data?.status == 'enabled'} - {:else if data.data?.status == 'disabled'} + {:else if data?.status == 'disabled'} {:else} @@ -19,13 +23,13 @@ - + - {data.data?.ads_percentage?.toFixed(1) || 'N.A.'}% + {(data?.ads_percentage || NaN).toFixed(1)}% - {data.data?.queries_today || 'N.A.'} + {data?.queries_today || 'N.A.'} diff --git a/src/lib/services/pihole/+service.ts b/src/lib/services/pihole/+service.ts index c7ff9b7..052df49 100644 --- a/src/lib/services/pihole/+service.ts +++ b/src/lib/services/pihole/+service.ts @@ -1,28 +1,29 @@ import type { ServiceConfig } from '$lib/config'; -import { type ServiceData, type ServiceHandler } from '../service'; +import type { ServicePoller } from '../service'; -export const handle: ServiceHandler = async (config: ServiceConfig) => { - const res: ServiceData = { - logo: 'https://cdn.rawgit.com/pi-hole/graphics/master/Vortex/Vortex.svg', - subtitle: 'Sends ads into a black hole' - }; +export const config: Partial = { + title: 'Pi-hole', + logo: 'https://cdn.rawgit.com/pi-hole/graphics/master/Vortex/Vortex.svg', + subtitle: 'Sends ads into a black hole' +}; +export const poll: ServicePoller = async (config: ServiceConfig) => { try { const resp: Response = await fetch( config.url + '/api.php?summaryRaw&auth=' + config.api_token ); - res.status = resp.ok ? 'online' : 'offline'; const raw = await resp.json(); - res.data = { - queries_today: raw.dns_queries_today, - ads_percentage: raw.ads_percentage_today, - status: raw.status, - client: raw.unique_clients + return { + status: resp.ok ? 'online' : 'offline', + data: { + queries_today: raw.dns_queries_today, + ads_percentage: raw.ads_percentage_today, + status: raw.status, + client: raw.unique_clients + } }; } catch (error) { - res.status = 'offline'; console.warn('could not fetch pihole status: ' + error); + return { status: 'offline' }; } - - return res; }; diff --git a/src/lib/services/prowlarr/+service.ts b/src/lib/services/prowlarr/+service.ts index ebd1f55..6ce6b5f 100644 --- a/src/lib/services/prowlarr/+service.ts +++ b/src/lib/services/prowlarr/+service.ts @@ -1,5 +1,7 @@ -import type { ServiceHandler } from '../service'; +import type { ServiceConfig } from '$lib/config'; -export const handle: ServiceHandler = () => { - return { logo: 'https://cdn.rawgit.com/Prowlarr/Prowlarr/develop/Logo/Prowlarr.svg' }; +export const config: Partial = { + title: 'Prowlarr', + logo: 'https://cdn.rawgit.com/Prowlarr/Prowlarr/develop/Logo/Prowlarr.svg', + subtitle: 'Indexer of indexer' }; diff --git a/src/lib/services/radarr/+service.ts b/src/lib/services/radarr/+service.ts index 3b6a133..f3cca03 100644 --- a/src/lib/services/radarr/+service.ts +++ b/src/lib/services/radarr/+service.ts @@ -1,8 +1,7 @@ -import type { ServiceHandler } from '../service'; +import type { ServiceConfig } from '$lib/config'; -export const handle: ServiceHandler = () => { - return { - logo: 'https://cdn.rawgit.com/Radarr/Radarr/develop/Logo/Radarr.svg', - subtitle: 'TV Shows Tracker' - }; +export const config: Partial = { + title: 'Sonarr', + logo: 'https://cdn.rawgit.com/Radarr/Radarr/develop/Logo/Radarr.svg', + subtitle: 'TV Shows Tracker' }; diff --git a/src/lib/services/service.ts b/src/lib/services/service.ts index 6252423..025a151 100644 --- a/src/lib/services/service.ts +++ b/src/lib/services/service.ts @@ -3,10 +3,9 @@ import type { ServiceConfig } from '$lib/config'; export type ServiceStatus = 'online' | 'offline'; export interface ServiceData { - status?: ServiceStatus; + status: ServiceStatus; - [x: string]: unknown; + data?: Record; } -export type ServiceHandler = (input: ServiceConfig) => ServiceData | Promise; -export type AsyncServiceHandler = (input: ServiceConfig) => Promise; +export type ServicePoller = (input: ServiceConfig) => Promise; diff --git a/src/lib/services/services.ts b/src/lib/services/services.ts index ddbac4a..d199450 100644 --- a/src/lib/services/services.ts +++ b/src/lib/services/services.ts @@ -1,21 +1,45 @@ -import type { ServiceHandler } from './service'; +import type { ServiceConfig } from '$lib/config'; +import type { ServiceData, ServicePoller } from './service'; -const services: Record = {}; - -const components: Record = {}; - -function registerService(type: string, handler: ServiceHandler) { - services[type] = handler; +interface ServiceRecord { + poll: ServicePoller; + config: Partial; } +const services: Record = {}; + +//eslint-disable-next-line @typescript-eslint/no-explicit-any +const components: Record = {}; + +async function pollURL(config: ServiceConfig): Promise { + try { + const resp = await fetch(config.url); + return { status: resp.ok ? 'online' : 'offline' }; + } catch (error) { + console.warn(`could not poll '${config.url}': ` + error); + return { status: 'offline' }; + } +} + +function registerService(type: string, service: Partial) { + const record: ServiceRecord = { + poll: service.poll || pollURL, + config: service.config || {} + }; + + services[type] = record; +} + +//eslint-disable-next-line @typescript-eslint/no-explicit-any function registerComponent(type: string, component: any) { components[type] = component; } -export function getServiceHandler(type: string): ServiceHandler | undefined { - return services[type]; +export function getServiceRecord(type: string): ServiceRecord { + return services[type] || { poll: pollURL, config: {} }; } +//eslint-disable-next-line @typescript-eslint/no-explicit-any export function getServiceComponent(type: string): any { return components[type]; } @@ -25,12 +49,13 @@ export async function initServices() { for (const [modulePath, load] of Object.entries(services)) { try { - const { handle } = (await load()) as any; - if (handle == undefined) { - throw new Error(`${modulePath} does not export 'handle'`); + //eslint-disable-next-line @typescript-eslint/no-explicit-any + const { poll, config } = (await load()) as any; + if (poll == undefined && config == undefined) { + throw new Error(`${modulePath} does not export 'poll' or 'config'`); } const typeName = modulePath.slice(18, -12); - registerService(typeName, handle); + registerService(typeName, { poll, config }); } catch (err) { console.error(`Could not load service definition from '${modulePath}': ${err}`); } @@ -42,6 +67,7 @@ export async function initComponents() { for (const [componentPath, load] of Object.entries(services)) { try { + //eslint-disable-next-line @typescript-eslint/no-explicit-any const module = (await load()) as any; if (module == undefined) { diff --git a/src/lib/services/sonarr/+content.svelte b/src/lib/services/sonarr/+content.svelte index b7895de..473d34a 100644 --- a/src/lib/services/sonarr/+content.svelte +++ b/src/lib/services/sonarr/+content.svelte @@ -1,8 +1,8 @@
diff --git a/src/lib/services/sonarr/+service.ts b/src/lib/services/sonarr/+service.ts index 7f93155..a19fa3e 100644 --- a/src/lib/services/sonarr/+service.ts +++ b/src/lib/services/sonarr/+service.ts @@ -1,11 +1,12 @@ import type { ServiceConfig } from '$lib/config'; -import type { ServiceHandler } from '../service'; +import type { ServicePoller, ServiceStatus } from '../service'; interface Status { warnings: number; errors: number; } +//eslint-disable-next-line @typescript-eslint/no-explicit-any function buildStatus(statuses: any[]): Status { let warnings = 0; let errors = 0; @@ -26,6 +27,8 @@ interface Queue { nextDate?: Date; total: number; } + +//eslint-disable-next-line @typescript-eslint/no-explicit-any function recordEstimatedCompletionTime(record: any): number { if (record.estimatedCompletionTime == undefined) { return Infinity; @@ -33,6 +36,7 @@ function recordEstimatedCompletionTime(record: any): number { return new Date(record.estimatedCompletionTime).getTime(); } +//eslint-disable-next-line @typescript-eslint/no-explicit-any function buildQueue(queue: any): Queue { if (queue?.records?.length === 0) { return { total: 0 }; @@ -51,12 +55,13 @@ function buildQueue(queue: any): Queue { }; } -export const handle: ServiceHandler = async (config: ServiceConfig) => { - const res = { - logo: 'https://cdn.rawgit.com/Sonarr/Sonarr/develop/Logo/Sonarr.svg', - subtitle: 'TV Show tracker' - }; +export const config: Partial = { + title: 'Sonarr', + logo: 'https://cdn.rawgit.com/Sonarr/Sonarr/develop/Logo/Sonarr.svg', + subtitle: 'TV Show tracker' +}; +export const poll: ServicePoller = async (config: ServiceConfig) => { const params = '?apikey=' + config.api_key; const requests = [ @@ -65,21 +70,22 @@ export const handle: ServiceHandler = async (config: ServiceConfig) => { ]; const [health, queue] = await Promise.allSettled(requests); - res.status = 'online'; + let status: ServiceStatus = 'online'; + const data: Record = {}; if (health.status != 'fulfilled') { - console.warn("Could not fetch '" + config.url + "' status: " + health.value); - res.status = 'offline'; + console.warn("Could not fetch '" + config.url + "' status: " + health.reason); + status = 'offline'; } else if (health.value.ok == true) { - res.health = buildStatus(await health.value.json()); + data.health = buildStatus(await health.value.json()); } if (queue.status != 'fulfilled') { - console.warn("Could not fetch '" + config.url + "' queue: " + queue.value); - res.status = 'offline'; + console.warn("Could not fetch '" + config.url + "' queue: " + queue.reason); + data.status = 'offline'; } else if (queue.value.ok == true) { - res.queue = buildQueue(await queue.value.json()); + data.queue = buildQueue(await queue.value.json()); } - return res; + return { status, data }; }; diff --git a/static/config.yml b/static/config.yml deleted file mode 100644 index de9dbac..0000000 --- a/static/config.yml +++ /dev/null @@ -1,14 +0,0 @@ -title: 'Hello World !!' -subtitle: 'actually, I am a new pilot.' - -services: - - title: '/Cloud' - subtitle: 'Private Cloud Utilities' - icon: 'fas fa-cloud' - items: - - title: 'NAS' - subtitle: 'Network Attached Storage' - icon: 'fas fa-hard-drive' - target: '_blank' - url: '/NAS' - keywords: 'cloud storage files'