refactor: reduce verbosity and dead code across new integrations

- Remove dead widget.audiobookshelf/navidrome i18n blocks (76 lines)
- Simplify audio-stats router (remove duplicated kind casts)
- Remove redundant defaultDisplayOptions in audio-stats component
- Dedup fetch helpers in uptime-kuma (fetchOkAsync) and
  paperless-ngx (fetchAuthenticatedAsync)
- Simplify navidrome asArray helper and getArtistCountAsync
- Simplify uptime-kuma averageUptimePercent and status derivation
This commit is contained in:
Thomas Camlong committed 2026-06-09 21:28:02 +02:00
1 parent ad76ce9cdc
commit fe35a9cf68
6 files changed
+34 -173

No files matched your search

+8 -22
View File
@@ -1,40 +1,26 @@
import type { AudiobookshelfDashboardData, NavidromeDashboardData } from "@homarr/integrations/types";
import { audiobookshelfRequestHandler } from "@homarr/request-handler/audiobookshelf";
import { navidromeRequestHandler } from "@homarr/request-handler/navidrome";
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, publicProcedure } from "../../trpc";
const requestHandlerByKind = {
navidrome: navidromeRequestHandler,
audiobookshelf: audiobookshelfRequestHandler,
} as const;
export const audioStatsRouter = createTRPCRouter({
getStats: publicProcedure
.concat(createOneIntegrationMiddleware("query", "navidrome", "audiobookshelf"))
.query(async ({ ctx }) => {
const fetchStatsByKind = {
const { kind } = ctx.integration;
const fetchByKind = {
navidrome: async () => {
const navidromeIntegration = { ...ctx.integration, kind: "navidrome" as const };
const innerHandler = requestHandlerByKind.navidrome.handler(navidromeIntegration, {});
const data = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
return {
kind: "navidrome" as const,
data: data.data as NavidromeDashboardData,
};
const handler = navidromeRequestHandler.handler({ ...ctx.integration, kind: "navidrome" as const }, {});
return (await handler.getCachedOrUpdatedDataAsync({ forceUpdate: false })).data;
},
audiobookshelf: async () => {
const audiobookshelfIntegration = { ...ctx.integration, kind: "audiobookshelf" as const };
const innerHandler = requestHandlerByKind.audiobookshelf.handler(audiobookshelfIntegration, {});
const data = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
return {
kind: "audiobookshelf" as const,
data: data.data as AudiobookshelfDashboardData,
};
const handler = audiobookshelfRequestHandler.handler({ ...ctx.integration, kind: "audiobookshelf" as const }, {});
return (await handler.getCachedOrUpdatedDataAsync({ forceUpdate: false })).data;
},
} as const;
return fetchStatsByKind[ctx.integration.kind]();
return { kind, data: await fetchByKind[kind]() };
}),
});
@@ -19,17 +19,8 @@ const emptyLibraryMessage = "Library not found or empty";
const subsonicAuthErrorCodes = new Set([40, 41]);
const asArray = <TValue>(value: TValue | TValue[] | undefined): TValue[] => {
if (value === undefined) {
return [];
}
if (Array.isArray(value)) {
return value;
}
return [value];
};
const asArray = <TValue>(value: TValue | TValue[] | undefined): TValue[] =>
[value].flat().filter((item): item is TValue => item !== undefined);
export class NavidromeIntegration extends Integration {
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
@@ -78,13 +69,10 @@ export class NavidromeIntegration extends Integration {
private async getArtistCountAsync(): Promise<number> {
const response = await this.subsonicRequestAsync("/rest/getArtists.view", {}, { tolerateEmptyLibrary: true });
let count = 0;
for (const index of asArray(response.artists?.index)) {
count += asArray(index.artist).length;
}
return count;
return asArray(response.artists?.index).reduce(
(count, index) => count + asArray(index.artist).length,
0,
);
}
private async getAlbumAndSongCountsAsync(): Promise<{ albumCount: number; songCount: number }> {
@@ -39,29 +39,23 @@ export class PaperlessNgxIntegration extends Integration {
};
}
private async fetchStatisticsAsync() {
const url = this.url("/api/statistics/");
const response = await fetchWithTrustedCertificatesAsync(url, {
private async fetchAuthenticatedAsync(path: `/${string}`) {
const response = await fetchWithTrustedCertificatesAsync(this.url(path), {
headers: this.getAuthHeaders(),
});
if (!response.ok) {
throw new ResponseError(response);
}
return response;
}
private async fetchStatisticsAsync() {
const response = await this.fetchAuthenticatedAsync("/api/statistics/");
return paperlessNgxStatisticsSchema.parse(await response.json());
}
private async fetchCountAsync(path: `/${string}`) {
const url = this.url(path);
const response = await fetchWithTrustedCertificatesAsync(url, {
headers: this.getAuthHeaders(),
});
if (!response.ok) {
throw new ResponseError(response);
}
const response = await this.fetchAuthenticatedAsync(path);
return paperlessNgxPaginatedCountSchema.parse(await response.json()).count;
}
@@ -69,15 +69,9 @@ export class UptimeKumaIntegration extends Integration {
const latestHeartbeat = heartbeats.at(-1);
const uptimeKey = `${monitor.id}_24`;
const uptimeRaw = heartbeat.uptimeList[uptimeKey];
let uptimePercent24h: number | null = null;
if (typeof uptimeRaw === "number") {
uptimePercent24h = uptimeRaw * 100;
}
let status: UptimeKumaMonitorCategory = "paused";
if (latestHeartbeat) {
status = uptimeKumaHeartbeatCategoryMap[latestHeartbeat.status] ?? "down";
}
const uptimePercent24h = typeof uptimeRaw === "number" ? uptimeRaw * 100 : null;
const status: UptimeKumaMonitorCategory =
(latestHeartbeat && (uptimeKumaHeartbeatCategoryMap[latestHeartbeat.status] ?? "down")) || "paused";
return {
id: monitor.id,
@@ -100,11 +94,8 @@ export class UptimeKumaIntegration extends Integration {
.map((monitor) => monitor.uptimePercent24h)
.filter((value): value is number => value !== null);
let averageUptimePercent = 0;
if (uptimeValues.length > 0) {
const totalUptime = uptimeValues.reduce((sum, value) => sum + value, 0);
averageUptimePercent = totalUptime / uptimeValues.length;
}
const averageUptimePercent =
uptimeValues.reduce((sum, value) => sum + value, 0) / Math.max(uptimeValues.length, 1);
return {
totalMonitors: monitors.length,
@@ -116,24 +107,20 @@ export class UptimeKumaIntegration extends Integration {
};
}
private async getStatusPageAsync() {
const response = await fetchWithTrustedCertificatesAsync(this.statusPageUrl());
private async fetchOkAsync(url: string | URL) {
const response = await fetchWithTrustedCertificatesAsync(url);
if (!response.ok) {
throw new ResponseError(response);
}
return response;
}
private async getHeartbeatAsync() {
const response = await fetchWithTrustedCertificatesAsync(this.heartbeatUrl());
private getStatusPageAsync() {
return this.fetchOkAsync(this.statusPageUrl());
}
if (!response.ok) {
throw new ResponseError(response);
}
return response;
private getHeartbeatAsync() {
return this.fetchOkAsync(this.heartbeatUrl());
}
private statusPageUrl() {
-76
View File
@@ -3220,82 +3220,6 @@
"internalServerError": "Failed to fetch audio stats data"
}
},
"audiobookshelf": {
"name": "Audiobookshelf",
"description": "Statistics from your Audiobookshelf instance",
"option": {
"showLibraryCount": {
"label": "Show library count",
"description": "Show the number of libraries on your instance"
},
"showAudiobooks": {
"label": "Show audiobooks",
"description": "Show the total number of audiobooks across all libraries"
},
"showPodcasts": {
"label": "Show podcasts",
"description": "Show the total number of podcasts across all libraries"
},
"showListeningTime": {
"label": "Show listening time",
"description": "Show your total listening time"
},
"showActiveSessions": {
"label": "Show active sessions",
"description": "Show the number of currently active playback sessions"
},
"compactMode": {
"label": "Compact mode",
"description": "Use a denser layout with smaller tiles, ideal for small widget sizes"
}
},
"libraryCount": "Libraries",
"audiobooks": "Audiobooks",
"podcasts": "Podcasts",
"listeningTime": "Listening Time",
"activeSessions": "Active Sessions",
"error": {
"internalServerError": "Failed to fetch Audiobookshelf data"
}
},
"navidrome": {
"name": "Navidrome",
"description": "Statistics from your Navidrome music server",
"option": {
"showArtists": {
"label": "Show artists",
"description": "Show the total number of artists in your library"
},
"showAlbums": {
"label": "Show albums",
"description": "Show the total number of albums in your library"
},
"showSongs": {
"label": "Show songs",
"description": "Show the total number of songs in your library"
},
"showNowPlaying": {
"label": "Show now playing",
"description": "Show tracks currently being played"
},
"showNowPlayingList": {
"label": "Show now playing list",
"description": "Show individual tracks instead of only the active stream count"
},
"maxNowPlayingItems": {
"label": "Max now playing items",
"description": "Maximum number of tracks to show in the now playing list"
}
},
"artists": "Artists",
"albums": "Albums",
"songs": "Songs",
"nowPlaying": "Now Playing",
"nowPlayingCount": "{count, plural, one {# stream} other {# streams}}",
"error": {
"internalServerError": "Failed to fetch Navidrome data"
}
},
"systemResources": {
"name": "System resources",
"description": "CPU, Memory, Disk and other hardware usage of your system",
+1 -19
View File
@@ -4,29 +4,11 @@ import { clientApi } from "@homarr/api/client";
import type { WidgetComponentProps } from "../definition";
import { AudioStatsContent } from "./audio-stats-content";
import type { AudioStatsDisplayOptions } from "./shared";
const defaultDisplayOptions: AudioStatsDisplayOptions = {
showArtists: true,
showAlbums: true,
showSongs: true,
showNowPlaying: true,
showNowPlayingList: true,
maxNowPlayingItems: 3,
showLibraryCount: true,
showAudiobooks: true,
showPodcasts: true,
showListeningTime: true,
showActiveSessions: true,
compactMode: false,
};
export default function AudioStatsWidget({ integrationIds, options, width }: WidgetComponentProps<"audioStats">) {
const [response] = clientApi.widget.audioStats.getStats.useSuspenseQuery({
integrationId: integrationIds[0] ?? "",
});
const displayOptions = { ...defaultDisplayOptions, ...options };
return <AudioStatsContent backend={response.kind} stats={response.data} options={displayOptions} width={width} />;
return <AudioStatsContent backend={response.kind} stats={response.data} options={options} width={width} />;
}