diff --git a/CODEOWNERS b/CODEOWNERS
index addd37e8c..ddaa51316 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -19,6 +19,14 @@ packages/integrations/src/anchor @ohmzi
apps/docs/docs/integrations/archive-team-warrior @darbyjack
packages/integrations/src/archive-team-warrior @darbyjack
+# Bazarr - introduced in https://github.com/homarr-labs/homarr/pull/6156 by @Ayush7614
+apps/docs/docs/integrations/bazarr @Ayush7614
+apps/docs/docs/widgets/bazarr @Ayush7614
+packages/api/src/router/widgets/bazarr.ts @Ayush7614
+packages/integrations/src/bazarr @Ayush7614
+packages/request-handler/src/bazarr.ts @Ayush7614
+packages/widgets/src/bazarr @Ayush7614
+
# Audiobookshelf - introduced in https://github.com/homarr-labs/homarr/pull/5910 by @ajnart
apps/docs/docs/integrations/audiobookshelf @ajnart
packages/integrations/src/audiobookshelf @ajnart
diff --git a/apps/docs/docs/integrations/bazarr/img/api-key-settings.png b/apps/docs/docs/integrations/bazarr/img/api-key-settings.png
new file mode 100644
index 000000000..c3f203b58
Binary files /dev/null and b/apps/docs/docs/integrations/bazarr/img/api-key-settings.png differ
diff --git a/apps/docs/docs/integrations/bazarr/index.mdx b/apps/docs/docs/integrations/bazarr/index.mdx
new file mode 100644
index 000000000..f9400a45e
--- /dev/null
+++ b/apps/docs/docs/integrations/bazarr/index.mdx
@@ -0,0 +1,54 @@
+---
+title: 'Bazarr'
+description: "Bazarr is a companion application to Sonarr and Radarr that manages and downloads subtitles."
+hide_title: true
+---
+
+import { IntegrationHeader } from '@site/src/components/integrations/header';
+import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
+import { AddingIntegration } from '@site/src/components/integrations/adding';
+import { IntegrationSecrets } from '@site/src/components/integrations/secrets';
+import { bazarrIntegration } from '.';
+import { bazarrWidget } from '@site/docs/widgets/bazarr';
+
+
+
+Bazarr complements Sonarr and Radarr by managing subtitle downloads. The Homarr integration uses your Bazarr API key to fetch missing subtitle counts and health badges.
+
+### Screenshots
+
+import apiKeySettings from './img/api-key-settings.png';
+import widgetOverview from '@site/docs/widgets/bazarr/img/widget-overview.png';
+
+
+
+
API key location in Bazarr (Settings → General → Security):
+

+
+
+
Bazarr widget on a Homarr board:
+

+
+
+
+### Widgets & Capabilities
+
+
+### Adding the integration
+
+
+### Secrets
+You can find your Bazarr API key under **Settings → General → Security**.
+
+
diff --git a/apps/docs/docs/integrations/bazarr/index.ts b/apps/docs/docs/integrations/bazarr/index.ts
new file mode 100644
index 000000000..528f7ae77
--- /dev/null
+++ b/apps/docs/docs/integrations/bazarr/index.ts
@@ -0,0 +1,9 @@
+import { IntegrationDefinition } from "@site/src/types";
+
+export const bazarrIntegration: IntegrationDefinition = {
+ name: "Bazarr",
+ description:
+ "Bazarr is a companion application to Sonarr and Radarr that manages and downloads subtitles for your media library.",
+ iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/bazarr.svg",
+ path: "../../integrations/bazarr",
+};
diff --git a/apps/docs/docs/widgets/bazarr/img/widget-overview.png b/apps/docs/docs/widgets/bazarr/img/widget-overview.png
new file mode 100644
index 000000000..d5af06b69
Binary files /dev/null and b/apps/docs/docs/widgets/bazarr/img/widget-overview.png differ
diff --git a/apps/docs/docs/widgets/bazarr/index.mdx b/apps/docs/docs/widgets/bazarr/index.mdx
new file mode 100644
index 000000000..e262cfa75
--- /dev/null
+++ b/apps/docs/docs/widgets/bazarr/index.mdx
@@ -0,0 +1,41 @@
+---
+title: 'Bazarr'
+description: 'Displays missing subtitle counts and health indicators from your Bazarr instance.'
+hide_title: true
+---
+
+import { WidgetHeader } from '@site/src/components/widgets/header';
+import { AddingWidget } from '@site/src/components/widgets/adding';
+import { WidgetConfig } from '@site/src/components/widgets/configuration';
+import { WidgetIntegrations } from '@site/src/components/widgets/integrations';
+import { bazarrWidget } from '.';
+import { bazarrIntegration } from '@site/docs/integrations/bazarr';
+
+
+
+This widget shows subtitle status from your Bazarr instance, including missing episode and movie subtitles, provider issues, and health warnings.
+
+### Screenshots
+
+import widgetOverview from './img/widget-overview.png';
+
+
+

+
+
+### Supported Integrations
+
+
+
+### Adding the widget
+
+
+
+### Configuration
+
diff --git a/apps/docs/docs/widgets/bazarr/index.ts b/apps/docs/docs/widgets/bazarr/index.ts
new file mode 100644
index 000000000..8b5c27ef8
--- /dev/null
+++ b/apps/docs/docs/widgets/bazarr/index.ts
@@ -0,0 +1,37 @@
+import { WidgetDefinition } from "@site/src/types";
+import { IconBadgeCc } from "@tabler/icons-react";
+
+export const bazarrWidget: WidgetDefinition = {
+ icon: IconBadgeCc,
+ name: "Bazarr",
+ description: "Displays missing subtitle counts and health indicators from your Bazarr instance.",
+ path: "../../widgets/bazarr",
+ configuration: {
+ items: [
+ {
+ name: "Show missing episodes",
+ description: "Displays the number of episodes with missing subtitles",
+ values: { type: "boolean" },
+ defaultValue: "yes",
+ },
+ {
+ name: "Show missing movies",
+ description: "Displays the number of movies with missing subtitles",
+ values: { type: "boolean" },
+ defaultValue: "yes",
+ },
+ {
+ name: "Show provider issues",
+ description: "Displays the number of subtitle providers with issues",
+ values: { type: "boolean" },
+ defaultValue: "yes",
+ },
+ {
+ name: "Show health issues",
+ description: "Displays the number of Bazarr health issues",
+ values: { type: "boolean" },
+ defaultValue: "yes",
+ },
+ ],
+ },
+};
diff --git a/packages/api/src/mcp.ts b/packages/api/src/mcp.ts
index faf7a62be..266db0622 100644
--- a/packages/api/src/mcp.ts
+++ b/packages/api/src/mcp.ts
@@ -9,6 +9,7 @@ import { iconsRouter } from "./router/icons";
import { infoRouter } from "./router/info";
import { integrationRouter } from "./router/integration/integration-router";
import { inviteRouter } from "./router/invite";
+import { bazarrRouter } from "./router/widgets/bazarr";
import { serverSettingsRouter } from "./router/serverSettings";
import { beszelRouter } from "./router/widgets/beszel";
import { calendarRouter } from "./router/widgets/calendar";
@@ -29,6 +30,7 @@ export const mcpRouter = createTRPCRouter({
info: infoRouter,
integration: integrationRouter,
invite: inviteRouter,
+ bazarr: bazarrRouter,
serverSettings: serverSettingsRouter,
beszel: beszelRouter,
calendar: calendarRouter,
diff --git a/packages/api/src/router/widgets/bazarr.ts b/packages/api/src/router/widgets/bazarr.ts
new file mode 100644
index 000000000..c47ad4173
--- /dev/null
+++ b/packages/api/src/router/widgets/bazarr.ts
@@ -0,0 +1,21 @@
+import { bazarrBadgesRequestHandler } from "@homarr/request-handler/bazarr";
+
+import { createOneIntegrationMiddleware } from "../../middlewares/integration";
+import { createTRPCRouter, publicProcedure } from "../../trpc";
+
+export const bazarrRouter = createTRPCRouter({
+ getBadges: publicProcedure
+ .meta({
+ mcp: {
+ enabled: true,
+ description:
+ "Get missing subtitle counts, provider issues, and health warnings for a Bazarr integration. REQUIRED: integrationId from integration_all",
+ },
+ })
+ .concat(createOneIntegrationMiddleware("query", "bazarr"))
+ .query(async ({ ctx }) => {
+ const innerHandler = bazarrBadgesRequestHandler.handler(ctx.integration, {});
+ const data = await innerHandler.getDataAsync();
+ return data.data;
+ }),
+});
diff --git a/packages/api/src/router/widgets/index.ts b/packages/api/src/router/widgets/index.ts
index 88aa78d8e..27907e924 100644
--- a/packages/api/src/router/widgets/index.ts
+++ b/packages/api/src/router/widgets/index.ts
@@ -9,6 +9,7 @@ export const widgetRouter = createTRPCRouter({
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)),
+ bazarr: lazy(() => import("./bazarr").then((mod) => mod.bazarrRouter)),
notebook: lazy(() => import("./notebook").then((mod) => mod.notebookRouter)),
weather: lazy(() => import("./weather").then((mod) => mod.weatherRouter)),
app: lazy(() => import("./app").then((mod) => mod.appRouter)),
diff --git a/packages/definitions/src/docker-integration-match.ts b/packages/definitions/src/docker-integration-match.ts
index eb6476bc8..7b0ea4c10 100644
--- a/packages/definitions/src/docker-integration-match.ts
+++ b/packages/definitions/src/docker-integration-match.ts
@@ -33,6 +33,7 @@ const integrationAliases: Partial> =
paperlessNgx: ["paperless-ngx", "paperless"],
coolify: ["coolify"],
truenas: ["truenas"],
+ bazarr: ["bazarr"],
synology: ["synology", "diskstation"],
};
diff --git a/packages/definitions/src/docs/homarr-docs-sitemap.ts b/packages/definitions/src/docs/homarr-docs-sitemap.ts
index 2e87554c7..b83fff39b 100644
--- a/packages/definitions/src/docs/homarr-docs-sitemap.ts
+++ b/packages/definitions/src/docs/homarr-docs-sitemap.ts
@@ -284,6 +284,7 @@ export type HomarrDocumentationPath =
| "/docs/integrations/lidarr"
| "/docs/integrations/readarr"
| "/docs/integrations/prowlarr"
+ | "/docs/integrations/bazarr"
| "/docs/integrations/jellyfin"
| "/docs/integrations/emby"
| "/docs/integrations/plex"
@@ -359,6 +360,7 @@ export type HomarrDocumentationPath =
| "/docs/widgets/immich-server-stats"
| "/docs/widgets/immich-album-carousel"
| "/docs/widgets/paperless-ngx"
+ | "/docs/widgets/bazarr"
| "/docs/widgets/tracearr"
| "/docs/widgets/speedtest-tracker"
| "/docs/widgets/uptime-kuma"
diff --git a/packages/definitions/src/docs/integration-doc-slugs.ts b/packages/definitions/src/docs/integration-doc-slugs.ts
index 7d7b19e3b..9fd20050a 100644
--- a/packages/definitions/src/docs/integration-doc-slugs.ts
+++ b/packages/definitions/src/docs/integration-doc-slugs.ts
@@ -13,6 +13,7 @@ export const integrationDocSlugs: Record = {
lidarr: "lidarr",
readarr: "readarr",
prowlarr: "prowlarr",
+ bazarr: "bazarr",
jellyfin: "jellyfin",
emby: "emby",
plex: "plex",
diff --git a/packages/definitions/src/docs/widget-doc-slugs.ts b/packages/definitions/src/docs/widget-doc-slugs.ts
index 7863aadeb..635badb49 100644
--- a/packages/definitions/src/docs/widget-doc-slugs.ts
+++ b/packages/definitions/src/docs/widget-doc-slugs.ts
@@ -38,6 +38,7 @@ export const widgetDocSlugs: Record = {
"immich-serverStats": "immich-server-stats",
"immich-albumCarousel": "immich-album-carousel",
paperlessNgx: "paperless-ngx",
+ bazarr: "bazarr",
tracearr: "tracearr",
speedtestTracker: "speedtest-tracker",
uptimeKuma: "uptime-kuma",
diff --git a/packages/definitions/src/integration.ts b/packages/definitions/src/integration.ts
index 0d548f81a..c4d216024 100644
--- a/packages/definitions/src/integration.ts
+++ b/packages/definitions/src/integration.ts
@@ -137,6 +137,15 @@ export const integrationDefs = {
defaultPort: 9696,
apiKeySettingsPath: "/settings/general",
},
+ bazarr: {
+ name: "Bazarr",
+ secretKinds: [["apiKey"]],
+ iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/bazarr.svg",
+ category: ["subtitleManager"],
+ documentationUrl: createDocumentationLink("/docs/integrations/bazarr"),
+ defaultPort: 6767,
+ apiKeySettingsPath: "/settings/general",
+ },
jellyfin: {
name: "Jellyfin",
secretKinds: [["username", "password"], ["apiKey"]],
@@ -561,6 +570,7 @@ export const integrationCategories = [
"documents",
"mediaLibrary",
"uptimeMonitoring",
+ "subtitleManager",
"reverseProxy",
] as const;
diff --git a/packages/definitions/src/widget-integration-map.ts b/packages/definitions/src/widget-integration-map.ts
index 781c37ecd..8f59060e3 100644
--- a/packages/definitions/src/widget-integration-map.ts
+++ b/packages/definitions/src/widget-integration-map.ts
@@ -33,6 +33,7 @@ export const widgetIntegrationSupport: Partial Promise]>;
diff --git a/packages/integrations/src/bazarr/bazarr-integration.ts b/packages/integrations/src/bazarr/bazarr-integration.ts
new file mode 100644
index 000000000..8f224d6ea
--- /dev/null
+++ b/packages/integrations/src/bazarr/bazarr-integration.ts
@@ -0,0 +1,45 @@
+import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
+
+import type { IntegrationTestingInput } from "../base/integration";
+import { Integration } from "../base/integration";
+import { TestConnectionError } from "../base/test-connection/test-connection-error";
+import type { TestingResult } from "../base/test-connection/test-connection-service";
+import type { BazarrBadges } from "./bazarr-types";
+import { bazarrBadgesSchema, bazarrSystemStatusSchema } from "./bazarr-types";
+
+export class BazarrIntegration extends Integration {
+ protected async testingAsync(input: IntegrationTestingInput): Promise {
+ const response = await input.fetchAsync(this.url("/api/system/status"), {
+ headers: this.getAuthHeaders(),
+ });
+
+ if (!response.ok) {
+ return TestConnectionError.StatusResult(response);
+ }
+
+ bazarrSystemStatusSchema.parse(await response.json());
+ return { success: true };
+ }
+
+ public async getBadgesAsync(): Promise {
+ const response = await fetchWithTrustedCertificatesAsync(this.url("/api/badges"), {
+ headers: this.getAuthHeaders(),
+ timeout: 10000,
+ });
+
+ if (!response.ok) {
+ throw new Error(
+ `Failed to fetch Bazarr badges for ${this.integration.name} (${this.integration.id}): ${response.statusText}`,
+ );
+ }
+
+ return bazarrBadgesSchema.parse(await response.json());
+ }
+
+ private getAuthHeaders(): Record {
+ return {
+ "X-API-KEY": this.getSecretValue("apiKey"),
+ Accept: "application/json",
+ };
+ }
+}
diff --git a/packages/integrations/src/bazarr/bazarr-types.ts b/packages/integrations/src/bazarr/bazarr-types.ts
new file mode 100644
index 000000000..fe16dedba
--- /dev/null
+++ b/packages/integrations/src/bazarr/bazarr-types.ts
@@ -0,0 +1,19 @@
+import { z } from "zod/v4";
+
+export const bazarrBadgesSchema = z.object({
+ episodes: z.number(),
+ movies: z.number(),
+ providers: z.number(),
+ status: z.number(),
+ sonarr_signalr: z.string(),
+ radarr_signalr: z.string(),
+ announcements: z.number(),
+});
+
+export type BazarrBadges = z.infer;
+
+export const bazarrSystemStatusSchema = z.object({
+ data: z.object({
+ bazarr_version: z.string(),
+ }),
+});
diff --git a/packages/integrations/src/index.ts b/packages/integrations/src/index.ts
index fe79f2159..c07f875eb 100644
--- a/packages/integrations/src/index.ts
+++ b/packages/integrations/src/index.ts
@@ -42,6 +42,7 @@ export { NavidromeIntegration } from "./navidrome/navidrome-integration";
export { UptimeKumaIntegration } from "./uptime-kuma/uptime-kuma-integration";
export { UmamiIntegration } from "./umami/umami-integration";
export { PeaNutIntegration } from "./peanut/peanut-integration";
+export { BazarrIntegration } from "./bazarr/bazarr-integration";
export { TraefikIntegration } from "./traefik/traefik-integration";
// Types
@@ -82,6 +83,7 @@ export type { AudiobookshelfDashboardData } from "./audiobookshelf/audiobookshel
export type { NavidromeDashboardData } from "./navidrome/navidrome-types";
export type { UptimeKumaDashboardData } from "./uptime-kuma/uptime-kuma-types";
export type { UmamiVisitorStats } from "./umami/umami-types";
+export type { BazarrBadges } from "./bazarr/bazarr-types";
export type { TraefikDashboardData, TraefikProtocolSummary, TraefikResourceSummary } from "./traefik/traefik-types";
export type {
ArchiveTeamWarriorBandwidth,
diff --git a/packages/integrations/test/bazarr-unit.spec.ts b/packages/integrations/test/bazarr-unit.spec.ts
new file mode 100644
index 000000000..eed0f9082
--- /dev/null
+++ b/packages/integrations/test/bazarr-unit.spec.ts
@@ -0,0 +1,110 @@
+// @vitest-environment node
+
+import { Response } from "undici";
+import { beforeEach, describe, expect, test, vi } from "vitest";
+
+import { createDb } from "@homarr/db/test";
+import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
+
+import type { IntegrationSecret } from "../src/base/types";
+import { BazarrIntegration } from "../src/bazarr/bazarr-integration";
+
+vi.mock("@homarr/db", async (importActual) => {
+ const actual = await importActual();
+ return { ...actual, db: createDb() };
+});
+
+vi.mock("@homarr/core/infrastructure/certificates", async (importActual) => {
+ const actual = await importActual();
+ return {
+ ...actual,
+ getTrustedCertificateHostnamesAsync: vi.fn().mockResolvedValue([]),
+ };
+});
+
+vi.mock("@homarr/core/infrastructure/http", () => ({
+ fetchWithTrustedCertificatesAsync: vi.fn(),
+}));
+
+const TEST_URL = "https://bazarr.example.com";
+const API_KEY = "test-api-key";
+
+const mockFetch = vi.mocked(fetchWithTrustedCertificatesAsync);
+
+const createIntegration = (decryptedSecrets: IntegrationSecret[] = [{ kind: "apiKey", value: API_KEY }]) =>
+ new BazarrIntegration({
+ id: "test-bazarr",
+ name: "Test Bazarr",
+ url: TEST_URL,
+ externalUrl: null,
+ decryptedSecrets,
+ });
+
+beforeEach(() => {
+ mockFetch.mockReset();
+});
+
+describe("BazarrIntegration getBadgesAsync", () => {
+ test("maps badge counts from the Bazarr API", async () => {
+ mockFetch.mockImplementation(
+ () =>
+ Promise.resolve(
+ new Response(
+ JSON.stringify({
+ episodes: 12,
+ movies: 4,
+ providers: 1,
+ status: 2,
+ sonarr_signalr: "LIVE",
+ radarr_signalr: "",
+ announcements: 0,
+ }),
+ {
+ status: 200,
+ headers: { "content-type": "application/json" },
+ },
+ ),
+ ) as unknown as ReturnType,
+ );
+
+ const badges = await createIntegration().getBadgesAsync();
+
+ expect(badges).toStrictEqual({
+ episodes: 12,
+ movies: 4,
+ providers: 1,
+ status: 2,
+ sonarr_signalr: "LIVE",
+ radarr_signalr: "",
+ announcements: 0,
+ });
+ });
+
+ test("sends the API key in the X-API-KEY header", async () => {
+ mockFetch.mockImplementation(
+ () =>
+ Promise.resolve(
+ new Response(
+ JSON.stringify({
+ episodes: 0,
+ movies: 0,
+ providers: 0,
+ status: 0,
+ sonarr_signalr: "",
+ radarr_signalr: "",
+ announcements: 0,
+ }),
+ {
+ status: 200,
+ headers: { "content-type": "application/json" },
+ },
+ ),
+ ) as unknown as ReturnType,
+ );
+
+ await createIntegration().getBadgesAsync();
+
+ const [, requestInit] = mockFetch.mock.calls[0] ?? [];
+ expect(requestInit?.headers).toMatchObject({ "X-API-KEY": API_KEY });
+ });
+});
diff --git a/packages/request-handler/src/bazarr.ts b/packages/request-handler/src/bazarr.ts
new file mode 100644
index 000000000..56b391658
--- /dev/null
+++ b/packages/request-handler/src/bazarr.ts
@@ -0,0 +1,15 @@
+import { createIntegrationAsync } from "@homarr/integrations";
+import type { BazarrBadges } from "@homarr/integrations";
+
+import { createIntegrationRequestHandler } from "./lib/integration-request-handler";
+
+export const bazarrBadgesRequestHandler = createIntegrationRequestHandler<
+ BazarrBadges,
+ "bazarr",
+ Record
+>({
+ async requestAsync(integration) {
+ const integrationInstance = await createIntegrationAsync(integration);
+ return await integrationInstance.getBadgesAsync();
+ },
+});
diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json
index 50c516092..6b09833d4 100644
--- a/packages/translation/src/lang/en.json
+++ b/packages/translation/src/lang/en.json
@@ -1335,6 +1335,10 @@
"paperlessNgx": {
"name": "Paperless-ngx",
"description": "Document management system for archiving and organizing scanned documents"
+ },
+ "bazarr": {
+ "name": "Bazarr",
+ "description": "Companion application to Sonarr and Radarr that manages and downloads subtitles"
}
},
"media": {
@@ -2758,6 +2762,35 @@
"internalServerError": "Failed to fetch Paperless-ngx data"
}
},
+ "bazarr": {
+ "name": "Bazarr",
+ "description": "Overview of missing subtitles and health status from your Bazarr instance",
+ "episodes": "Missing Episodes",
+ "movies": "Missing Movies",
+ "providers": "Provider Issues",
+ "status": "Health Issues",
+ "option": {
+ "showMissingEpisodes": {
+ "label": "Show missing episodes",
+ "description": "Display the number of episodes with missing subtitles"
+ },
+ "showMissingMovies": {
+ "label": "Show missing movies",
+ "description": "Display the number of movies with missing subtitles"
+ },
+ "showProviderIssues": {
+ "label": "Show provider issues",
+ "description": "Display the number of subtitle providers with issues"
+ },
+ "showHealthIssues": {
+ "label": "Show health issues",
+ "description": "Display the number of Bazarr health issues"
+ }
+ },
+ "error": {
+ "internalServerError": "Failed to fetch Bazarr data"
+ }
+ },
"immich-serverStats": {
"name": "Immich Server Stats",
"description": "Information about your Immich instance",
diff --git a/packages/widgets/src/bazarr/component.module.css b/packages/widgets/src/bazarr/component.module.css
new file mode 100644
index 000000000..9b5857379
--- /dev/null
+++ b/packages/widgets/src/bazarr/component.module.css
@@ -0,0 +1,77 @@
+.root {
+ container-type: size;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ min-height: 0;
+ padding: clamp(6px, 3cqi, 14px);
+ gap: clamp(4px, 2cqi, 10px);
+ overflow: hidden;
+}
+
+.grid {
+ display: grid;
+ flex: 1;
+ min-height: 0;
+ gap: clamp(4px, 2cqi, 10px);
+ grid-template-columns: repeat(var(--stat-cols, 2), minmax(0, 1fr));
+ grid-auto-rows: 1fr;
+ align-content: stretch;
+}
+
+.statTile {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ min-height: 0;
+ min-width: 0;
+ gap: clamp(2px, 1.5cqi, 5px);
+ padding: clamp(4px, 2.5cqi, 10px);
+ border-radius: var(--mantine-radius-md);
+ background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
+ overflow: hidden;
+}
+
+.statTileWarning {
+ background-color: light-dark(var(--mantine-color-orange-0), var(--mantine-color-dark-5));
+}
+
+.statIcon {
+ color: var(--mantine-color-dimmed);
+ flex-shrink: 0;
+}
+
+.statLabel {
+ font-size: clamp(8px, 2.5cqi, 12px);
+ color: var(--mantine-color-dimmed);
+ line-height: 1.2;
+ text-align: center;
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.statValue {
+ font-size: clamp(14px, 5.5cqi, 24px);
+ font-weight: 700;
+ color: var(--mantine-primary-color-filled);
+ line-height: 1.1;
+ text-align: center;
+}
+
+.statValueWarning {
+ color: var(--mantine-color-orange-6);
+}
+
+.emptyState {
+ display: flex;
+ flex: 1;
+ align-items: center;
+ justify-content: center;
+ min-height: 0;
+ color: var(--mantine-color-dimmed);
+ font-size: clamp(11px, 3cqi, 14px);
+ text-align: center;
+}
diff --git a/packages/widgets/src/bazarr/component.tsx b/packages/widgets/src/bazarr/component.tsx
new file mode 100644
index 000000000..094c32ee1
--- /dev/null
+++ b/packages/widgets/src/bazarr/component.tsx
@@ -0,0 +1,103 @@
+"use client";
+
+import type { CSSProperties } from "react";
+import { Text } from "@mantine/core";
+import { IconAlertTriangle, IconDeviceTv, IconMovie, IconPlugConnected } from "@tabler/icons-react";
+
+import { clientApi } from "@homarr/api/client";
+import { useScopedI18n } from "@homarr/translation/client";
+
+import { WidgetEmptyState } from "../common/empty-state";
+import type { WidgetComponentProps } from "../definition";
+import classes from "./component.module.css";
+
+const statVisibilityByOption = {
+ showMissingEpisodes: "episodes",
+ showMissingMovies: "movies",
+ showProviderIssues: "providers",
+ showHealthIssues: "status",
+} as const;
+
+const statIcons = {
+ episodes: IconDeviceTv,
+ movies: IconMovie,
+ providers: IconPlugConnected,
+ status: IconAlertTriangle,
+} as const;
+
+const gridColsByWidth = [
+ { minWidth: 380, cols: 2 },
+ { minWidth: 0, cols: 1 },
+] as const;
+
+const iconSizeByWidth = [
+ { minWidth: 320, size: 22 },
+ { minWidth: 200, size: 18 },
+ { minWidth: 0, size: 16 },
+] as const;
+
+export default function BazarrWidget({ integrationIds, options, width }: WidgetComponentProps<"bazarr">) {
+ const t = useScopedI18n("widget.bazarr");
+ const { data: badges } = clientApi.widget.bazarr.getBadges.useQuery(
+ { integrationId: integrationIds[0] ?? "" },
+ { enabled: Boolean(integrationIds[0]) },
+ );
+
+ if (!badges) return ;
+
+ const statValues = {
+ episodes: badges.episodes,
+ movies: badges.movies,
+ providers: badges.providers,
+ status: badges.status,
+ } as const;
+
+ const visibleStatKeys = Object.entries(statVisibilityByOption)
+ .filter(([optionKey]) => options[optionKey as keyof typeof options])
+ .map(([, statKey]) => statKey);
+
+ const gridCols = getGridCols(width);
+ const iconSize = getIconSize(width);
+
+ if (visibleStatKeys.length === 0) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+ {visibleStatKeys.map((statKey) => {
+ const Icon = statIcons[statKey];
+ const value = statValues[statKey];
+ const isWarning = (statKey === "providers" || statKey === "status") && value > 0;
+
+ return (
+
+
+ {value}
+ {t(statKey)}
+
+ );
+ })}
+
+
+ );
+}
+
+function getGridCols(width: number): number {
+ const match = gridColsByWidth.find(({ minWidth }) => width >= minWidth);
+ return match?.cols ?? 1;
+}
+
+function getIconSize(width: number): number {
+ const match = iconSizeByWidth.find(({ minWidth }) => width >= minWidth);
+ return match?.size ?? 16;
+}
diff --git a/packages/widgets/src/bazarr/index.ts b/packages/widgets/src/bazarr/index.ts
new file mode 100644
index 000000000..f2e79f570
--- /dev/null
+++ b/packages/widgets/src/bazarr/index.ts
@@ -0,0 +1,36 @@
+import { IconBadgeCc, IconServerOff } from "@tabler/icons-react";
+
+import { createWidgetDefinition } from "../definition";
+import { optionsBuilder } from "../options";
+
+export const { definition, componentLoader } = createWidgetDefinition("bazarr", {
+ icon: IconBadgeCc,
+ supportedIntegrations: ["bazarr"],
+ integrationsRequired: true,
+ createOptions() {
+ return optionsBuilder.from((factory) => ({
+ showMissingEpisodes: factory.switch({
+ defaultValue: true,
+ withDescription: true,
+ }),
+ showMissingMovies: factory.switch({
+ defaultValue: true,
+ withDescription: true,
+ }),
+ showProviderIssues: factory.switch({
+ defaultValue: true,
+ withDescription: true,
+ }),
+ showHealthIssues: factory.switch({
+ defaultValue: true,
+ withDescription: true,
+ }),
+ }));
+ },
+ errors: {
+ INTERNAL_SERVER_ERROR: {
+ icon: IconServerOff,
+ message: (t) => t("widget.bazarr.error.internalServerError"),
+ },
+ },
+}).withDynamicImport(() => import("./component"));
diff --git a/packages/widgets/src/index.tsx b/packages/widgets/src/index.tsx
index 2b843425d..ca20ff3a1 100644
--- a/packages/widgets/src/index.tsx
+++ b/packages/widgets/src/index.tsx
@@ -12,6 +12,7 @@ import * as audioStats from "./audio-stats";
import * as app from "./app";
import * as archiveTeamWarrior from "./archive-team-warrior";
import * as bookmarks from "./bookmarks";
+import * as bazarr from "./bazarr";
import * as calendar from "./calendar";
import * as clock from "./clock";
import * as coolify from "./coolify";
@@ -94,6 +95,7 @@ export const widgetImports = {
networkControllerStatus,
rssFeed,
bookmarks,
+ bazarr,
indexerManager,
healthMonitoring,
mediaTranscoding,