diff --git a/public/locales/en/common.json b/public/locales/en/common.json index f0f6e6c5..d41e30ed 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -284,5 +284,13 @@ "96-night": "Thunderstorm With Hail", "99-day": "Thunderstorm With Hail", "99-night": "Thunderstorm With Hail" + }, + "homebridge": { + "available_update": "System", + "update_available": "Update Available", + "up_to_date": "Up to Date", + "available_homebridge_update": "Plugins", + "plugins_updates_available": "{{quantity}} Available", + "plugins_up_to_date": "Up to Date" } } diff --git a/src/widgets/components.js b/src/widgets/components.js index da6ce362..8f7bfeb9 100644 --- a/src/widgets/components.js +++ b/src/widgets/components.js @@ -9,6 +9,7 @@ const components = { docker: dynamic(() => import("./docker/component")), emby: dynamic(() => import("./emby/component")), gotify: dynamic(() => import("./gotify/component")), + homebridge: dynamic(() => import("./homebridge/component")), jackett: dynamic(() => import("./jackett/component")), jellyfin: dynamic(() => import("./emby/component")), jellyseerr: dynamic(() => import("./jellyseerr/component")), diff --git a/src/widgets/homebridge/component.jsx b/src/widgets/homebridge/component.jsx new file mode 100644 index 00000000..bc6dd87d --- /dev/null +++ b/src/widgets/homebridge/component.jsx @@ -0,0 +1,44 @@ +import { useTranslation } from "next-i18next"; + +import Container from "components/services/widget/container"; +import Block from "components/services/widget/block"; +import useWidgetAPI from "utils/proxy/use-widget-api"; + +export default function Component({ service }) { + const { t } = useTranslation(); + + const { widget } = service; + + const { data: homebridge, error: homebridgeError } = useWidgetAPI(widget, "info"); + + if (homebridgeError || (homebridge && !homebridge.data)) { + return ; + } + + if (!homebridge) { + return ( + + + + + + ); + } + + return ( + + + + + + ); +} diff --git a/src/widgets/homebridge/proxy.js b/src/widgets/homebridge/proxy.js new file mode 100644 index 00000000..7e666b53 --- /dev/null +++ b/src/widgets/homebridge/proxy.js @@ -0,0 +1,101 @@ +import cache from "memory-cache"; + +import { httpProxy } from "utils/proxy/http"; +import { formatApiCall } from "utils/proxy/api-helpers"; +import getServiceWidget from "utils/config/service-helpers"; +import createLogger from "utils/logger"; +import widgets from "widgets/widgets"; + +const proxyName = "homebridgeProxyHandler"; +const sessionTokenCacheKey = `${proxyName}__sessionToken`; +const logger = createLogger(proxyName); + +async function login(widget) { + const endpoint = "auth/login"; + const api = widgets?.[widget.type]?.api + const loginUrl = new URL(formatApiCall(api, { endpoint, ...widget })); + const loginBody = { username: widget.username, password: widget.password }; + const headers = { "Content-Type": "application/json" }; + const [status, contentType, data, responseHeaders] = await httpProxy(loginUrl, { + method: "POST", + body: JSON.stringify(loginBody), + headers, + }); + + const dataParsed = JSON.parse(data.toString()) + + cache.put(sessionTokenCacheKey, dataParsed.access_token); + + return { status, contentType, data: dataParsed, responseHeaders }; +} + +async function apiCall(widget, endpoint) { + const headers = { + "content-type": "application/json", + "Authorization": `Bearer ${cache.get(sessionTokenCacheKey)}`, + } + + const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget })); + const method = "GET"; + + let [status, contentType, data, responseHeaders] = await httpProxy(url, { + method, + headers, + }); + + if (status === 401) { + logger.debug("Homebridge is rejecting the request, but obtaining new session token"); + const { data: loginData } = login(widget); + headers.Authorization = loginData?.auth_token; + + // retry the request, now with the new session token + [status, contentType, data, responseHeaders] = await httpProxy(url, { + method, + headers, + }); + } + + if (status !== 200) { + logger.error("Error getting data from Homebridge: %d. Data: %s", status, data); + } + + return { status, contentType, data: JSON.parse(data.toString()), responseHeaders }; +} + +function formatPluginsResponse(plugins) { + const quantity = plugins?.data.filter(p => p.updateAvailable).length; + return { + updatesAvailable: quantity > 0, + quantity, + } +} + +export default async function homebridgeProxyHandler(req, res) { + const { group, service } = req.query; + + if (!group || !service) { + logger.debug("Invalid or missing service '%s' or group '%s'", service, group); + return res.status(400).json({ error: "Invalid proxy service type" }); + } + + const widget = await getServiceWidget(group, service); + + if (!widget) { + logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group); + return res.status(400).json({ error: "Invalid proxy service type" }); + } + + await login(widget); + + const statusRS = await apiCall(widget, "status/homebridge"); + const versionRS = await apiCall(widget, "status/homebridge-version"); + const pluginsRS = await apiCall(widget, "plugins"); + + return res.status(200).send({ + data: { + status: statusRS?.data?.status, + updateAvailable: versionRS?.data?.updateAvailable, + plugins: formatPluginsResponse(pluginsRS) + } + }); +} diff --git a/src/widgets/homebridge/widget.js b/src/widgets/homebridge/widget.js new file mode 100644 index 00000000..31d4f30e --- /dev/null +++ b/src/widgets/homebridge/widget.js @@ -0,0 +1,14 @@ +import homebridgeProxyHandler from "./proxy"; + +const widget = { + api: "{url}/api/{endpoint}", + proxyHandler: homebridgeProxyHandler, + + mappings: { + info: { + endpoint: "/", + } + }, +}; + +export default widget; diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js index a90bd620..55379e98 100644 --- a/src/widgets/widgets.js +++ b/src/widgets/widgets.js @@ -5,6 +5,7 @@ import changedetectionio from "./changedetectionio/widget"; import coinmarketcap from "./coinmarketcap/widget"; import emby from "./emby/widget"; import gotify from "./gotify/widget"; +import homebridge from "./homebridge/widget"; import jackett from "./jackett/widget"; import jellyseerr from "./jellyseerr/widget"; import lidarr from "./lidarr/widget"; @@ -39,6 +40,7 @@ const widgets = { coinmarketcap, emby, gotify, + homebridge, jackett, jellyfin: emby, jellyseerr,