feat: add Uptime Kuma, Audiobookshelf, Paperless-ngx, and Navidrome integrations
Add four new integrations with full widget support: - Uptime Kuma: dashboard monitors, uptime percentage ring, status counts - Audiobookshelf: library stats, audiobooks/podcasts counts, listening time - Paperless-ngx: document stats, inbox ratio ring, correspondents/tags - Navidrome: artist/album/song counts, now playing with track list Also adds a unified "Audio Stats" widget that merges Navidrome and Audiobookshelf into a single configurable widget with backend-specific option visibility. Includes Vitest specs for pagination, status mapping, and shared utils.
This commit is contained in:
53 files changed
+3370
-17
No files matched your search
@@ -0,0 +1,40 @@
|
||||
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 = {
|
||||
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,
|
||||
};
|
||||
},
|
||||
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,
|
||||
};
|
||||
},
|
||||
} as const;
|
||||
|
||||
return fetchStatsByKind[ctx.integration.kind]();
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { audiobookshelfRequestHandler } from "@homarr/request-handler/audiobookshelf";
|
||||
|
||||
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
|
||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
|
||||
export const audiobookshelfRouter = createTRPCRouter({
|
||||
getStats: publicProcedure.concat(createOneIntegrationMiddleware("query", "audiobookshelf")).query(async ({ ctx }) => {
|
||||
const innerHandler = audiobookshelfRequestHandler.handler(ctx.integration, {});
|
||||
const data = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
||||
return data.data;
|
||||
}),
|
||||
});
|
||||
@@ -6,6 +6,7 @@ export const widgetRouter = createTRPCRouter({
|
||||
anchorNotes: lazy(() => import("./anchor-notes").then((mod) => mod.anchorNotesRouter)),
|
||||
coolify: lazy(() => import("./coolify").then((mod) => mod.coolifyRouter)),
|
||||
immich: lazy(() => import("./immich").then((mod) => mod.immichRouter)),
|
||||
paperlessNgx: lazy(() => import("./paperless-ngx").then((mod) => mod.paperlessNgxRouter)),
|
||||
notebook: lazy(() => import("./notebook").then((mod) => mod.notebookRouter)),
|
||||
weather: lazy(() => import("./weather").then((mod) => mod.weatherRouter)),
|
||||
app: lazy(() => import("./app").then((mod) => mod.appRouter)),
|
||||
@@ -30,6 +31,10 @@ export const widgetRouter = createTRPCRouter({
|
||||
timetable: lazy(() => import("./timetable").then((mod) => mod.timetableRouter)),
|
||||
tracearr: lazy(() => import("./tracearr").then((mod) => mod.tracearrRouter)),
|
||||
speedtestTracker: lazy(() => import("./speedtest-tracker").then((mod) => mod.speedtestTrackerRouter)),
|
||||
uptimeKuma: lazy(() => import("./uptime-kuma").then((mod) => mod.uptimeKumaRouter)),
|
||||
audioStats: lazy(() => import("./audio-stats").then((mod) => mod.audioStatsRouter)),
|
||||
audiobookshelf: lazy(() => import("./audiobookshelf").then((mod) => mod.audiobookshelfRouter)),
|
||||
navidrome: lazy(() => import("./navidrome").then((mod) => mod.navidromeRouter)),
|
||||
umami: lazy(() => import("./umami").then((mod) => mod.umamiRouter)),
|
||||
ups: lazy(() => import("./ups").then((mod) => mod.upsRouter)),
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { navidromeRequestHandler } from "@homarr/request-handler/navidrome";
|
||||
|
||||
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
|
||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
|
||||
export const navidromeRouter = createTRPCRouter({
|
||||
getStats: publicProcedure
|
||||
.concat(createOneIntegrationMiddleware("query", "navidrome"))
|
||||
.input(
|
||||
z.object({
|
||||
integrationId: z.string(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ ctx }) => {
|
||||
const innerHandler = navidromeRequestHandler.handler(ctx.integration, {});
|
||||
const data = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
||||
return data.data;
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { paperlessNgxStatsRequestHandler } from "@homarr/request-handler/paperless-ngx";
|
||||
|
||||
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
|
||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
|
||||
export const paperlessNgxRouter = createTRPCRouter({
|
||||
getStats: publicProcedure.concat(createOneIntegrationMiddleware("query", "paperlessNgx")).query(async ({ ctx }) => {
|
||||
const innerHandler = paperlessNgxStatsRequestHandler.handler(ctx.integration, {});
|
||||
const data = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
||||
return data.data;
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import { observable } from "@trpc/server/observable";
|
||||
|
||||
import type { UptimeKumaDashboardData } from "@homarr/integrations/types";
|
||||
import { uptimeKumaRequestHandler } from "@homarr/request-handler/uptime-kuma";
|
||||
|
||||
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
|
||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
|
||||
export const uptimeKumaRouter = createTRPCRouter({
|
||||
getDashboard: publicProcedure
|
||||
.concat(createManyIntegrationMiddleware("query", "uptimeKuma"))
|
||||
.query(async ({ ctx }) => {
|
||||
const results = await Promise.all(
|
||||
ctx.integrations.map(async (integration) => {
|
||||
const innerHandler = uptimeKumaRequestHandler.handler(integration, {});
|
||||
const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
||||
|
||||
return {
|
||||
integrationId: integration.id,
|
||||
integrationName: integration.name,
|
||||
integrationUrl: integration.url,
|
||||
dashboard: data,
|
||||
updatedAt: timestamp,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return results;
|
||||
}),
|
||||
|
||||
subscribeToDashboard: publicProcedure
|
||||
.concat(createManyIntegrationMiddleware("query", "uptimeKuma"))
|
||||
.subscription(({ ctx }) => {
|
||||
return observable<{
|
||||
integrationId: string;
|
||||
dashboard: UptimeKumaDashboardData;
|
||||
timestamp: Date;
|
||||
}>((emit) => {
|
||||
const unsubscribes = ctx.integrations.map((integration) => {
|
||||
const innerHandler = uptimeKumaRequestHandler.handler(integration, {});
|
||||
return innerHandler.subscribe((dashboard) => {
|
||||
emit.next({
|
||||
integrationId: integration.id,
|
||||
dashboard,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||
};
|
||||
});
|
||||
}),
|
||||
});
|
||||
Reference in new issue
Block a user