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:
6 files changed
+34
-173
No files matched your search
@@ -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() {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
Reference in new issue
Block a user