diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 62ef2102..308f1e63 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -298,5 +298,9 @@ "rejectedPushes": "Rejected", "filters": "Filters", "indexers": "Indexers" + }, + "scripted": { + "yes": "Yes", + "no": "No" } } diff --git a/src/widgets/components.js b/src/widgets/components.js index f46808a5..27d3c14e 100644 --- a/src/widgets/components.js +++ b/src/widgets/components.js @@ -30,6 +30,7 @@ const components = { readarr: dynamic(() => import("./readarr/component")), rutorrent: dynamic(() => import("./rutorrent/component")), sabnzbd: dynamic(() => import("./sabnzbd/component")), + scripted: dynamic(() => import("./scripted/component")), sonarr: dynamic(() => import("./sonarr/component")), speedtest: dynamic(() => import("./speedtest/component")), strelaysrv: dynamic(() => import("./strelaysrv/component")), diff --git a/src/widgets/scripted/README.md b/src/widgets/scripted/README.md new file mode 100644 index 00000000..cf16f770 --- /dev/null +++ b/src/widgets/scripted/README.md @@ -0,0 +1,50 @@ +# Widget for displaying scripted information + +This widget executes a script and displays the script's output. The executed script must return +the data in json format. The returned fields can be filtered, labeled and formatted. + +For example to display the online status and number of players of a minecraft server the +configuration could be: + +```yaml + widget: + type: scripted + script: "mcstatus myserver:25565 json" + fields: [ "online", "player_count", "ping" ] + field_labels: + player_count: "players" + field_types: + online: boolean + ping: + type: common.ms + style: unit + unit: millisecond + unitDisplay: narrow +``` + +The output of the executed script of the above example is + +```json +{ "online": true, "version": "1.19.2", "protocol": 760, "motd": "A Minecraft server", "player_count": 0, "player_max": 20, "players": [], "ping": 0.527 } +``` + +From the scripts output the three fields online, player_count and ping +will be displayed. The field player_count will be named "players". The fields online +and ping will be formatted. + +## Configuration + +* **script** is the script that will be executed as the user that runs the homepage server. + It's output must be in JSON format. + +* **fields** names the fields from the script's output that shall be displayed. + It is recommended to set the fields. Otherwise the widget will be empty if no data can be displayed. + +* **field_labels** defines the labels to be displayed for the fields. The field name itself is used if there + is no label for a field, like for the fields online and ping in the example. + +* **field_types** defines the types of the fields. If unset then the value is shown unformatted. + The type's value can either be a simple string like common.number or a map if multiple + configuration options shall be used like in the example above for the ping field. + See the "common.XY" translation strings in the file public/locales/en/common.json for + supported field types. In addition the type boolean is supported. diff --git a/src/widgets/scripted/component.jsx b/src/widgets/scripted/component.jsx new file mode 100644 index 00000000..095fd180 --- /dev/null +++ b/src/widgets/scripted/component.jsx @@ -0,0 +1,49 @@ +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: scriptedData, error: scriptedError } = useWidgetAPI(widget, ''); + + if (scriptedError) { + return ; + } + + if (!scriptedData) { + return ( + + {widget.fields?.map(field => ( + + ))} + + ); + } + + (scriptedData || []).forEach(e => { + if (e.type && e.value !== undefined) { + if (typeof e.type === 'object') { + e.typedValue = t(e.type.type, {...e.type, type: null, value: e.value}); + } else if (e.type == 'boolean') { + e.typedValue = t(e.value ? 'scripted.yes' : 'scripted.no'); + } else { + e.typedValue = t(e.type, { value: e.value }); + } + } else { + e.typedValue = e.value; + } + }) + + return ( +
+ {(scriptedData || []).map(e => ( + + ))} +
+ ); +} diff --git a/src/widgets/scripted/proxy.js b/src/widgets/scripted/proxy.js new file mode 100644 index 00000000..3d75fb7d --- /dev/null +++ b/src/widgets/scripted/proxy.js @@ -0,0 +1,36 @@ +import getServiceWidget from "utils/config/service-helpers"; +import createLogger from "utils/logger"; + +const logger = createLogger("scriptedProxyHandler"); +const { execSync } = require('child_process') + +export default async function scriptedProxyHandler(req, res, map) { + const { group, service, endpoint } = req.query; + + if (group && service) { + const widget = await getServiceWidget(group, service); + + let output; + try { + output = JSON.parse(execSync(widget.script).toString()); + } + catch (err) { + return res.status(500).send('script failed: ' + err); + } + + const fields = widget.fields || Object.keys(output) || []; + const labels = widget.field_labels || []; + const types = widget.field_types || []; + + const result = fields.map(field => { + return { + name: field, label: labels[field] || field, value: output[field], type: types[field] + }; + }); + + return res.status(200).json(result); + } + + logger.debug("Invalid or missing proxy service type '%s' in group '%s'", service, group); + return res.status(400).json({ error: "Invalid proxy service type" }); +} diff --git a/src/widgets/scripted/widget.js b/src/widgets/scripted/widget.js new file mode 100644 index 00000000..8d682145 --- /dev/null +++ b/src/widgets/scripted/widget.js @@ -0,0 +1,7 @@ +import scriptedProxyHandler from "./proxy"; + +const widget = { + proxyHandler: scriptedProxyHandler, +}; + +export default widget; diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js index 577243a7..41525461 100644 --- a/src/widgets/widgets.js +++ b/src/widgets/widgets.js @@ -25,6 +25,7 @@ import radarr from "./radarr/widget"; import readarr from "./readarr/widget"; import rutorrent from "./rutorrent/widget"; import sabnzbd from "./sabnzbd/widget"; +import scripted from './scripted/widget'; import sonarr from "./sonarr/widget"; import speedtest from "./speedtest/widget"; import strelaysrv from "./strelaysrv/widget"; @@ -62,6 +63,7 @@ const widgets = { readarr, rutorrent, sabnzbd, + scripted, sonarr, speedtest, strelaysrv,