mirror of
https://github.com/gethomepage/homepage.git
synced 2025-12-06 21:57:48 +01:00
Compare commits
13 Commits
v1.5.0
...
3f8da51aeb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f8da51aeb | ||
|
|
837717461f | ||
|
|
effedc28ed | ||
|
|
76b477572e | ||
|
|
6c6660b91b | ||
|
|
6886040798 | ||
|
|
1fb4850bef | ||
|
|
06cf76d724 | ||
|
|
7aeda56af4 | ||
|
|
2058b7fcae | ||
|
|
1e06e93e47 | ||
|
|
8f756d4084 | ||
|
|
02089a35ee |
@@ -178,3 +178,32 @@ See [ClusterRole and ClusterRoleBinding](../installation/k8s.md#clusterrole-and-
|
|||||||
## Caveats
|
## Caveats
|
||||||
|
|
||||||
Similarly to Docker service discovery, there currently is no rigid ordering to discovered services and discovered services will be displayed above those specified in the `services.yaml`.
|
Similarly to Docker service discovery, there currently is no rigid ordering to discovered services and discovered services will be displayed above those specified in the `services.yaml`.
|
||||||
|
|
||||||
|
## Adding extra configuration files
|
||||||
|
|
||||||
|
Some Homepage features (for example, [Proxmox](../configs/proxmox.md)) require additional configuration files such as `proxmox.yaml`.
|
||||||
|
When running Homepage on Kubernetes, these files must be provided via a `ConfigMap` and mounted into the container at `/app/config`.
|
||||||
|
|
||||||
|
### ConfigMap example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: homepage
|
||||||
|
data:
|
||||||
|
proxmox.yaml: |
|
||||||
|
pve:
|
||||||
|
url: https://proxmox.host.or.ip:8006
|
||||||
|
token: username@pam!Token ID
|
||||||
|
secret: secret
|
||||||
|
```
|
||||||
|
|
||||||
|
Mount the file into `/app/config` by updating the `Deployment`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /app/config/proxmox.yaml
|
||||||
|
name: homepage-config
|
||||||
|
subPath: proxmox.yaml
|
||||||
|
```
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ description: Proxmox Configuration
|
|||||||
---
|
---
|
||||||
|
|
||||||
The Proxmox connection is configured in the `proxmox.yaml` file. See [Create token](#create-token) section below for details on how to generate the required API token.
|
The Proxmox connection is configured in the `proxmox.yaml` file. See [Create token](#create-token) section below for details on how to generate the required API token.
|
||||||
You can configure multiple nodes - be sure to use the exact `proxmoxNode` identifier!
|
To configure multiple nodes, ensure the key name in the `proxmox.yaml` matches the `proxmoxNode` field used in your service configuration.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
pve:
|
pve: # must match your actual Proxmox node name
|
||||||
url: https://proxmox.host.or.ip:8006
|
url: https://proxmox.host.or.ip:8006
|
||||||
token: username@pam!Token ID
|
token: username@pam!Token ID
|
||||||
secret: secret
|
secret: secret
|
||||||
|
|||||||
@@ -16,4 +16,5 @@ widget:
|
|||||||
username: username
|
username: username
|
||||||
password: password
|
password: password
|
||||||
enableLeechProgress: true # optional, defaults to false
|
enableLeechProgress: true # optional, defaults to false
|
||||||
|
enableLeechSize: true # optional, defaults to false
|
||||||
```
|
```
|
||||||
|
|||||||
28
docs/widgets/services/yourspotify.md
Normal file
28
docs/widgets/services/yourspotify.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
title: Your Spotify
|
||||||
|
description: Your Spotify Widget Configuration
|
||||||
|
---
|
||||||
|
|
||||||
|
Learn more about [Your Spotify](https://github.com/Yooooomi/your_spotify).
|
||||||
|
|
||||||
|
Find your API key under `Settings > Account > Public token`, click `Generate` if not yet generated, copy key after
|
||||||
|
`?token=`.
|
||||||
|
|
||||||
|
Allowed fields: `["songs", "time", "artists"]`.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
widget:
|
||||||
|
type: yourspotify
|
||||||
|
url: http://your-spotify-server.host.or.ip # if using lsio image, add /api/
|
||||||
|
key: apikeyapikeyapikeyapikeyapikey
|
||||||
|
interval: month # optional, defaults to week
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Interval
|
||||||
|
|
||||||
|
Allowed values for `interval`: `day`, `week`, `month`, `year`, `all`.
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
|
||||||
|
`interval` is different from predefined intervals you see in `Your Spotify`'s UI.
|
||||||
|
For example, `This week` in UI means _from the start of this week_, here `week` means _past 7 days_.
|
||||||
@@ -176,6 +176,7 @@ nav:
|
|||||||
- widgets/services/wgeasy.md
|
- widgets/services/wgeasy.md
|
||||||
- widgets/services/whatsupdocker.md
|
- widgets/services/whatsupdocker.md
|
||||||
- widgets/services/xteve.md
|
- widgets/services/xteve.md
|
||||||
|
- widgets/services/yourspotify.md
|
||||||
- widgets/services/zabbix.md
|
- widgets/services/zabbix.md
|
||||||
- "Information Widgets":
|
- "Information Widgets":
|
||||||
- widgets/info/index.md
|
- widgets/info/index.md
|
||||||
|
|||||||
10
package.json
10
package.json
@@ -17,8 +17,8 @@
|
|||||||
"compare-versions": "^6.1.1",
|
"compare-versions": "^6.1.1",
|
||||||
"dockerode": "^4.0.7",
|
"dockerode": "^4.0.7",
|
||||||
"follow-redirects": "^1.15.11",
|
"follow-redirects": "^1.15.11",
|
||||||
"gamedig": "^5.3.1",
|
"gamedig": "^5.3.2",
|
||||||
"i18next": "^24.2.3",
|
"i18next": "^25.5.3",
|
||||||
"ical.js": "^2.1.0",
|
"ical.js": "^2.1.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"json-rpc-2.0": "^1.7.0",
|
"json-rpc-2.0": "^1.7.0",
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
"next-i18next": "^12.1.0",
|
"next-i18next": "^12.1.0",
|
||||||
"ping": "^0.4.4",
|
"ping": "^0.4.4",
|
||||||
"pretty-bytes": "^6.1.1",
|
"pretty-bytes": "^6.1.1",
|
||||||
"raw-body": "^3.0.0",
|
"raw-body": "^3.0.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-i18next": "^15.5.3",
|
"react-i18next": "^15.5.3",
|
||||||
@@ -44,13 +44,13 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
"@tailwindcss/postcss": "^4.0.9",
|
"@tailwindcss/postcss": "^4.1.14",
|
||||||
"eslint": "^9.25.1",
|
"eslint": "^9.25.1",
|
||||||
"eslint-config-next": "^15.2.4",
|
"eslint-config-next": "^15.2.4",
|
||||||
"eslint-config-prettier": "^10.1.1",
|
"eslint-config-prettier": "^10.1.1",
|
||||||
"eslint-plugin-import": "^2.32.0",
|
"eslint-plugin-import": "^2.32.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||||
"eslint-plugin-prettier": "^5.5.1",
|
"eslint-plugin-prettier": "^5.5.4",
|
||||||
"eslint-plugin-react": "^7.37.4",
|
"eslint-plugin-react": "^7.37.4",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
|
|||||||
562
pnpm-lock.yaml
generated
562
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1114,5 +1114,10 @@
|
|||||||
"num_success_latest": "Succeeding",
|
"num_success_latest": "Succeeding",
|
||||||
"num_failure_latest": "Failing",
|
"num_failure_latest": "Failing",
|
||||||
"bytes_added_30": "Bytes Added"
|
"bytes_added_30": "Bytes Added"
|
||||||
|
},
|
||||||
|
"yourspotify": {
|
||||||
|
"songs": "Songs",
|
||||||
|
"time": "Time",
|
||||||
|
"artists": "Artists"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export default function QueueEntry({ title, activity, timeLeft, progress }) {
|
export default function QueueEntry({ title, activity, timeLeft, progress, size }) {
|
||||||
return (
|
return (
|
||||||
<div className="text-theme-700 dark:text-theme-200 relative h-5 rounded-md bg-theme-200/50 dark:bg-theme-900/20 m-1 px-1 flex">
|
<div className="text-theme-700 dark:text-theme-200 relative h-5 rounded-md bg-theme-200/50 dark:bg-theme-900/20 m-1 px-1 flex">
|
||||||
<div
|
<div
|
||||||
@@ -11,6 +11,7 @@ export default function QueueEntry({ title, activity, timeLeft, progress }) {
|
|||||||
<div className="absolute w-full whitespace-nowrap text-ellipsis overflow-hidden text-left">{title}</div>
|
<div className="absolute w-full whitespace-nowrap text-ellipsis overflow-hidden text-left">{title}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="self-center text-xs flex justify-end mr-1.5 pl-1 z-10 text-ellipsis overflow-hidden whitespace-nowrap">
|
<div className="self-center text-xs flex justify-end mr-1.5 pl-1 z-10 text-ellipsis overflow-hidden whitespace-nowrap">
|
||||||
|
{size && `${size} - `}
|
||||||
{timeLeft ? `${activity} - ${timeLeft}` : activity}
|
{timeLeft ? `${activity} - ${timeLeft}` : activity}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -417,6 +417,7 @@ function Home({ initialSettings }) {
|
|||||||
)}
|
)}
|
||||||
<meta name="msapplication-TileColor" content={themes[settings.color || "slate"][settings.theme || "dark"]} />
|
<meta name="msapplication-TileColor" content={themes[settings.color || "slate"][settings.theme || "dark"]} />
|
||||||
<meta name="theme-color" content={themes[settings.color || "slate"][settings.theme || "dark"]} />
|
<meta name="theme-color" content={themes[settings.color || "slate"][settings.theme || "dark"]} />
|
||||||
|
<meta name="color-scheme" content="dark light"></meta>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<Script src="/api/config/custom.js" />
|
<Script src="/api/config/custom.js" />
|
||||||
@@ -539,38 +540,48 @@ export default function Wrapper({ initialSettings, fallback }) {
|
|||||||
html.classList.add(desiredThemeClass);
|
html.classList.add(desiredThemeClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove any previously applied inline styles
|
if (backgroundImage) {
|
||||||
body.style.backgroundImage = "";
|
const safeBackgroundImage = backgroundImage.replace(/'/g, "\\'");
|
||||||
body.style.backgroundColor = "";
|
body.style.backgroundImage = `linear-gradient(rgb(var(--bg-color) / ${opacity}), rgb(var(--bg-color) / ${opacity})), url('${safeBackgroundImage}')`;
|
||||||
body.style.backgroundAttachment = "";
|
body.style.backgroundSize = "cover";
|
||||||
|
body.style.backgroundPosition = "center";
|
||||||
|
body.style.backgroundAttachment = "fixed";
|
||||||
|
body.style.backgroundRepeat = "no-repeat";
|
||||||
|
body.style.backgroundColor = "";
|
||||||
|
} else {
|
||||||
|
body.style.backgroundImage = "none";
|
||||||
|
body.style.backgroundColor = "rgb(var(--bg-color))";
|
||||||
|
body.style.backgroundSize = "";
|
||||||
|
body.style.backgroundPosition = "";
|
||||||
|
body.style.backgroundAttachment = "";
|
||||||
|
body.style.backgroundRepeat = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
body.style.backgroundImage = "";
|
||||||
|
body.style.backgroundColor = "";
|
||||||
|
body.style.backgroundSize = "";
|
||||||
|
body.style.backgroundPosition = "";
|
||||||
|
body.style.backgroundAttachment = "";
|
||||||
|
body.style.backgroundRepeat = "";
|
||||||
|
};
|
||||||
}, [backgroundImage, opacity, theme, color, initialSettings.color]);
|
}, [backgroundImage, opacity, theme, color, initialSettings.color]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div id="page_wrapper" className="relative min-h-screen">
|
||||||
{backgroundImage && (
|
<div
|
||||||
<div
|
id="inner_wrapper"
|
||||||
id="background"
|
tabIndex="-1"
|
||||||
aria-hidden="true"
|
className={classNames(
|
||||||
style={{
|
"w-full min-h-screen overflow-auto",
|
||||||
backgroundImage: `linear-gradient(rgb(var(--bg-color) / ${opacity}), rgb(var(--bg-color) / ${opacity})), url('${backgroundImage}')`,
|
backgroundBlur &&
|
||||||
}}
|
`backdrop-blur${initialSettings.background.blur?.length ? `-${initialSettings.background.blur}` : ""}`,
|
||||||
/>
|
backgroundSaturate && `backdrop-saturate-${initialSettings.background.saturate}`,
|
||||||
)}
|
backgroundBrightness && `backdrop-brightness-${initialSettings.background.brightness}`,
|
||||||
<div id="page_wrapper" className="relative h-full">
|
)}
|
||||||
<div
|
>
|
||||||
id="inner_wrapper"
|
<Index initialSettings={initialSettings} fallback={fallback} />
|
||||||
tabIndex="-1"
|
|
||||||
className={classNames(
|
|
||||||
"w-full h-full overflow-auto",
|
|
||||||
backgroundBlur &&
|
|
||||||
`backdrop-blur${initialSettings.background.blur?.length ? `-${initialSettings.background.blur}` : ""}`,
|
|
||||||
backgroundSaturate && `backdrop-saturate-${initialSettings.background.saturate}`,
|
|
||||||
backgroundBrightness && `backdrop-brightness-${initialSettings.background.brightness}`,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Index initialSettings={initialSettings} fallback={fallback} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,18 +30,6 @@ body,
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background-color: rgb(var(--bg-color));
|
|
||||||
}
|
|
||||||
|
|
||||||
#background {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 0;
|
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-attachment: scroll;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
|
|||||||
@@ -284,6 +284,7 @@ export function cleanServiceGroups(groups) {
|
|||||||
|
|
||||||
// deluge, qbittorrent
|
// deluge, qbittorrent
|
||||||
enableLeechProgress,
|
enableLeechProgress,
|
||||||
|
enableLeechSize,
|
||||||
|
|
||||||
// diskstation
|
// diskstation
|
||||||
volume,
|
volume,
|
||||||
@@ -408,6 +409,9 @@ export function cleanServiceGroups(groups) {
|
|||||||
// wgeasy
|
// wgeasy
|
||||||
threshold,
|
threshold,
|
||||||
|
|
||||||
|
// yourspotify
|
||||||
|
interval,
|
||||||
|
|
||||||
// technitium
|
// technitium
|
||||||
range,
|
range,
|
||||||
|
|
||||||
@@ -490,6 +494,7 @@ export function cleanServiceGroups(groups) {
|
|||||||
}
|
}
|
||||||
if (["deluge", "qbittorrent"].includes(type)) {
|
if (["deluge", "qbittorrent"].includes(type)) {
|
||||||
if (enableLeechProgress !== undefined) widget.enableLeechProgress = JSON.parse(enableLeechProgress);
|
if (enableLeechProgress !== undefined) widget.enableLeechProgress = JSON.parse(enableLeechProgress);
|
||||||
|
if (enableLeechSize !== undefined) widget.enableLeechSize = JSON.parse(enableLeechSize);
|
||||||
}
|
}
|
||||||
if (["opnsense", "pfsense"].includes(type)) {
|
if (["opnsense", "pfsense"].includes(type)) {
|
||||||
if (wan) widget.wan = wan;
|
if (wan) widget.wan = wan;
|
||||||
@@ -623,6 +628,11 @@ export function cleanServiceGroups(groups) {
|
|||||||
if (pool3) widget.pool3 = pool3;
|
if (pool3) widget.pool3 = pool3;
|
||||||
if (pool4) widget.pool4 = pool4;
|
if (pool4) widget.pool4 = pool4;
|
||||||
}
|
}
|
||||||
|
if (type === "yourspotify") {
|
||||||
|
if (interval !== undefined) {
|
||||||
|
widget.interval = interval;
|
||||||
|
}
|
||||||
|
}
|
||||||
return widget;
|
return widget;
|
||||||
});
|
});
|
||||||
return cleanedService;
|
return cleanedService;
|
||||||
|
|||||||
@@ -24,10 +24,11 @@ function buildResponse(plans) {
|
|||||||
|
|
||||||
plans.forEach((plan) => {
|
plans.forEach((plan) => {
|
||||||
const statuses = plan?.recentBackups?.status;
|
const statuses = plan?.recentBackups?.status;
|
||||||
|
// See https://github.com/garethgeorge/backrest/blob/4357295a17cb2e71639473c9929a060c4dd1b624/proto/v1/operations.proto#L78-L87
|
||||||
if (Array.isArray(statuses) && statuses.length > 0) {
|
if (Array.isArray(statuses) && statuses.length > 0) {
|
||||||
if (statuses[0] === "STATUS_SUCCESS") {
|
if (statuses[0] === "STATUS_SUCCESS") {
|
||||||
numSuccessLatest++;
|
numSuccessLatest++;
|
||||||
} else {
|
} else if (statuses[0] === "STATUS_ERROR") {
|
||||||
numFailureLatest++;
|
numFailureLatest++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ const components = {
|
|||||||
wgeasy: dynamic(() => import("./wgeasy/component")),
|
wgeasy: dynamic(() => import("./wgeasy/component")),
|
||||||
whatsupdocker: dynamic(() => import("./whatsupdocker/component")),
|
whatsupdocker: dynamic(() => import("./whatsupdocker/component")),
|
||||||
xteve: dynamic(() => import("./xteve/component")),
|
xteve: dynamic(() => import("./xteve/component")),
|
||||||
|
yourspotify: dynamic(() => import("./yourspotify/component")),
|
||||||
zabbix: dynamic(() => import("./zabbix/component")),
|
zabbix: dynamic(() => import("./zabbix/component")),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,11 @@ export default function Component({ service }) {
|
|||||||
timeLeft={t("common.duration", { value: queueEntry.eta })}
|
timeLeft={t("common.duration", { value: queueEntry.eta })}
|
||||||
title={queueEntry.name}
|
title={queueEntry.name}
|
||||||
activity={queueEntry.state}
|
activity={queueEntry.state}
|
||||||
|
size={
|
||||||
|
widget?.enableLeechSize
|
||||||
|
? t("common.bbytes", { value: queueEntry.size, maximumFractionDigits: 1 })
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
key={`${queueEntry.name}-${queueEntry.amount_left}`}
|
key={`${queueEntry.name}-${queueEntry.amount_left}`}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export default function Component({ service }) {
|
|||||||
let status;
|
let status;
|
||||||
let uptime = 0;
|
let uptime = 0;
|
||||||
let logIndex = 0;
|
let logIndex = 0;
|
||||||
|
const hasLogs = Array.isArray(monitor.logs) && monitor.logs.length > 0;
|
||||||
|
|
||||||
switch (monitor.status) {
|
switch (monitor.status) {
|
||||||
case 0:
|
case 0:
|
||||||
@@ -62,7 +63,7 @@ export default function Component({ service }) {
|
|||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
status = t("uptimerobot.up");
|
status = t("uptimerobot.up");
|
||||||
uptime = t("common.duration", { value: monitor.logs[0].duration });
|
uptime = t("common.duration", { value: hasLogs ? monitor.logs[0].duration : 0 });
|
||||||
logIndex = 1;
|
logIndex = 1;
|
||||||
break;
|
break;
|
||||||
case 8:
|
case 8:
|
||||||
@@ -76,14 +77,14 @@ export default function Component({ service }) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastDown = new Date(monitor.logs[logIndex].datetime * 1000).toLocaleString();
|
const lastDown = hasLogs ? new Date(monitor.logs[logIndex].datetime * 1000).toLocaleString() : "";
|
||||||
const downDuration = t("common.duration", { value: monitor.logs[logIndex].duration });
|
const downDuration = t("common.duration", { value: hasLogs ? monitor.logs[logIndex].duration : 0 });
|
||||||
const hideDown = logIndex === 1 && monitor.logs[logIndex].type !== 1;
|
const hideDown = !hasLogs || (logIndex === 1 && monitor.logs[logIndex].type !== 1);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container service={service}>
|
<Container service={service}>
|
||||||
<Block label="uptimerobot.status" value={status} />
|
<Block label="uptimerobot.status" value={status} />
|
||||||
<Block label="uptimerobot.uptime" value={uptime} />
|
{hasLogs && <Block label="uptimerobot.uptime" value={uptime} />}
|
||||||
{!hideDown && <Block label="uptimerobot.lastDown" value={lastDown} />}
|
{!hideDown && <Block label="uptimerobot.lastDown" value={lastDown} />}
|
||||||
{!hideDown && <Block label="uptimerobot.downDuration" value={downDuration} />}
|
{!hideDown && <Block label="uptimerobot.downDuration" value={downDuration} />}
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ import watchtower from "./watchtower/widget";
|
|||||||
import wgeasy from "./wgeasy/widget";
|
import wgeasy from "./wgeasy/widget";
|
||||||
import whatsupdocker from "./whatsupdocker/widget";
|
import whatsupdocker from "./whatsupdocker/widget";
|
||||||
import xteve from "./xteve/widget";
|
import xteve from "./xteve/widget";
|
||||||
|
import yourspotify from "./yourspotify/widget";
|
||||||
import zabbix from "./zabbix/widget";
|
import zabbix from "./zabbix/widget";
|
||||||
|
|
||||||
const widgets = {
|
const widgets = {
|
||||||
@@ -291,6 +292,7 @@ const widgets = {
|
|||||||
wgeasy,
|
wgeasy,
|
||||||
whatsupdocker,
|
whatsupdocker,
|
||||||
xteve,
|
xteve,
|
||||||
|
yourspotify,
|
||||||
zabbix,
|
zabbix,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
76
src/widgets/yourspotify/component.jsx
Normal file
76
src/widgets/yourspotify/component.jsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import Block from "components/services/widget/block";
|
||||||
|
import Container from "components/services/widget/container";
|
||||||
|
import { useTranslation } from "next-i18next";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||||
|
|
||||||
|
function getStartDate(interval) {
|
||||||
|
const d = new Date();
|
||||||
|
switch (interval) {
|
||||||
|
case "day":
|
||||||
|
d.setDate(d.getDate() - 1);
|
||||||
|
break;
|
||||||
|
case "week":
|
||||||
|
d.setDate(d.getDate() - 7);
|
||||||
|
break;
|
||||||
|
case "month":
|
||||||
|
d.setMonth(d.getMonth() - 1);
|
||||||
|
break;
|
||||||
|
case "year":
|
||||||
|
d.setFullYear(d.getFullYear() - 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return d.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Component({ service }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { widget } = service;
|
||||||
|
|
||||||
|
const interval = widget?.interval || "week";
|
||||||
|
|
||||||
|
const date = useMemo(() => {
|
||||||
|
return interval === "all" ? "2006-04-23T00:00:00.000Z" : getStartDate(interval);
|
||||||
|
}, [interval]);
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
timeSplit: "all",
|
||||||
|
start: date,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data: songsListened, error: songsError } = useWidgetAPI(widget, "songs", params);
|
||||||
|
const { data: timeListened, error: timeError } = useWidgetAPI(widget, "time", params);
|
||||||
|
const { data: artistsListened, error: artistsError } = useWidgetAPI(widget, "artists", params);
|
||||||
|
|
||||||
|
if (songsError || timeError || artistsError) {
|
||||||
|
return <Container service={service} error={songsError ?? timeError ?? artistsError} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNaN(songsListened) || isNaN(timeListened) || isNaN(artistsListened)) {
|
||||||
|
return (
|
||||||
|
<Container service={service}>
|
||||||
|
<Block label="yourspotify.songs" />
|
||||||
|
<Block label="yourspotify.time" />
|
||||||
|
<Block label="yourspotify.artists" />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container service={service}>
|
||||||
|
<Block label="yourspotify.songs" value={t("common.number", { value: songsListened })} />
|
||||||
|
|
||||||
|
<Block
|
||||||
|
label="yourspotify.time"
|
||||||
|
value={t(
|
||||||
|
timeListened > 0 ? "common.duration" : "common.number", // Display 0 if duration is 0
|
||||||
|
{
|
||||||
|
value: timeListened / 1000,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Block label="yourspotify.artists" value={t("common.number", { value: artistsListened })} />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
27
src/widgets/yourspotify/widget.js
Normal file
27
src/widgets/yourspotify/widget.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { asJson } from "utils/proxy/api-helpers";
|
||||||
|
import genericProxyHandler from "utils/proxy/handlers/generic";
|
||||||
|
|
||||||
|
const widget = {
|
||||||
|
api: "{url}/spotify/{endpoint}?token={key}",
|
||||||
|
proxyHandler: genericProxyHandler,
|
||||||
|
|
||||||
|
mappings: {
|
||||||
|
songs: {
|
||||||
|
endpoint: "songs_per",
|
||||||
|
params: ["start", "timeSplit"],
|
||||||
|
map: (data) => asJson(data)[0]?.count || 0,
|
||||||
|
},
|
||||||
|
time: {
|
||||||
|
endpoint: "time_per",
|
||||||
|
params: ["start", "timeSplit"],
|
||||||
|
map: (data) => asJson(data)[0]?.count || 0,
|
||||||
|
},
|
||||||
|
artists: {
|
||||||
|
endpoint: "different_artists_per",
|
||||||
|
params: ["start", "timeSplit"],
|
||||||
|
map: (data) => asJson(data)[0]?.artists?.length || 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default widget;
|
||||||
Reference in New Issue
Block a user