diff --git a/docs/widgets/services/stash.md b/docs/widgets/services/stash.md
new file mode 100644
index 00000000..b2d3e0ef
--- /dev/null
+++ b/docs/widgets/services/stash.md
@@ -0,0 +1,20 @@
+---
+title: Stash
+description: Stash Widget Configuration
+---
+
+Learn more about [Stash](https://github.com/stashapp/stash).
+
+Find your API key from inside Stash at `Settings > Security > API Key`. Note that the API key is only required if your Stash instance has login credentials.
+
+Allowed fields: `["scenes", "scenesPlayed", "playCount", "playDuration", "sceneSize", "sceneDuration", "images", "imageSize", "galleries", "performers", "studios", "movies", "tags", "oCount"]`.
+
+If more than 4 fields are provided, only the first 4 are displayed.
+
+```yaml
+widget:
+ type: stash
+ url: http://stash.host.or.ip
+ key: stashapikey
+ fields: ["scenes", "images"] # optional - default fields shown
+```
diff --git a/mkdocs.yml b/mkdocs.yml
index c6ecfda9..4c574e18 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -124,6 +124,7 @@ nav:
- widgets/services/scrutiny.md
- widgets/services/sonarr.md
- widgets/services/speedtest-tracker.md
+ - widgets/services/stash.md
- widgets/services/syncthing-relay-server.md
- widgets/services/tailscale.md
- widgets/services/tdarr.md
diff --git a/public/locales/en/common.json b/public/locales/en/common.json
index 7d5097fb..f6c1b841 100644
--- a/public/locales/en/common.json
+++ b/public/locales/en/common.json
@@ -836,5 +836,21 @@
"notifications": "Notifications",
"issues": "Issues",
"pulls": "Pull Requests"
+ },
+ "stash": {
+ "scenes": "Scenes",
+ "scenesPlayed": "Scenes Played",
+ "playCount": "Total Plays",
+ "playDuration": "Time Watched",
+ "sceneSize": "Scenes Size",
+ "sceneDuration": "Scenes Duration",
+ "images": "Images",
+ "imageSize": "Images Size",
+ "galleries": "Galleries",
+ "performers": "Performers",
+ "studios": "Studios",
+ "movies": "Movies",
+ "tags": "Tags",
+ "oCount": "O Count"
}
}
diff --git a/src/widgets/components.js b/src/widgets/components.js
index 9054c4d2..74d5fe1f 100644
--- a/src/widgets/components.js
+++ b/src/widgets/components.js
@@ -97,6 +97,7 @@ const components = {
scrutiny: dynamic(() => import("./scrutiny/component")),
sonarr: dynamic(() => import("./sonarr/component")),
speedtest: dynamic(() => import("./speedtest/component")),
+ stash: dynamic(() => import("./stash/component")),
strelaysrv: dynamic(() => import("./strelaysrv/component")),
tailscale: dynamic(() => import("./tailscale/component")),
tautulli: dynamic(() => import("./tautulli/component")),
diff --git a/src/widgets/stash/component.jsx b/src/widgets/stash/component.jsx
new file mode 100644
index 00000000..66f949c1
--- /dev/null
+++ b/src/widgets/stash/component.jsx
@@ -0,0 +1,62 @@
+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: stats, error: stashError } = useWidgetAPI(widget, "stats");
+
+ if (stashError) {
+ return ;
+ }
+
+ if (!stats) {
+ return (
+
+
+
+
+ );
+ }
+
+ // Provide a default if not set in the config
+ if (!widget.fields) {
+ widget.fields = ["scenes", "images"];
+ }
+
+ // Limit to a maximum of 4 at a time
+ if (widget.fields.length > 4) {
+ widget.fields = widget.fields.slice(0, 4);
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/widgets/stash/widget.js b/src/widgets/stash/widget.js
new file mode 100644
index 00000000..82803c72
--- /dev/null
+++ b/src/widgets/stash/widget.js
@@ -0,0 +1,40 @@
+import { asJson } from "utils/proxy/api-helpers";
+import genericProxyHandler from "utils/proxy/handlers/generic";
+
+const widget = {
+ api: "{url}/{endpoint}?apikey={key}",
+ proxyHandler: genericProxyHandler,
+
+ mappings: {
+ stats: {
+ method: "POST",
+ endpoint: "graphql",
+ headers: {
+ "content-type": "application/json",
+ },
+ body: JSON.stringify({
+ query: `{
+ stats {
+ scene_count
+ scenes_size
+ scenes_duration
+ image_count
+ images_size
+ gallery_count
+ performer_count
+ studio_count
+ movie_count
+ tag_count
+ total_o_count
+ total_play_duration
+ total_play_count
+ scenes_played
+ }
+ }`,
+ }),
+ map: (data) => asJson(data).data.stats,
+ },
+ },
+};
+
+export default widget;
diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js
index 5804253d..2eae4ba9 100644
--- a/src/widgets/widgets.js
+++ b/src/widgets/widgets.js
@@ -89,6 +89,7 @@ import sabnzbd from "./sabnzbd/widget";
import scrutiny from "./scrutiny/widget";
import sonarr from "./sonarr/widget";
import speedtest from "./speedtest/widget";
+import stash from "./stash/widget";
import strelaysrv from "./strelaysrv/widget";
import tailscale from "./tailscale/widget";
import tautulli from "./tautulli/widget";
@@ -201,6 +202,7 @@ const widgets = {
scrutiny,
sonarr,
speedtest,
+ stash,
strelaysrv,
tailscale,
tautulli,