widget refactoring
This commit is contained in:
parent
03fa2f86d7
commit
035dd25ece
29 changed files with 851 additions and 10 deletions
|
@ -1,5 +1,3 @@
|
|||
import { URLSearchParams } from "next/dist/compiled/@edge-runtime/primitives/url";
|
||||
|
||||
import createLogger from "utils/logger";
|
||||
import genericProxyHandler from "utils/proxies/generic";
|
||||
import widgets from "widgets/widgets";
|
||||
|
@ -35,10 +33,9 @@ export default async function handler(req, res) {
|
|||
|
||||
if (req.query.params) {
|
||||
const queryParams = JSON.parse(req.query.params);
|
||||
const query = new URLSearchParams(mappingParams.map(p => [p, queryParams[p]]));
|
||||
const query = new URLSearchParams(mappingParams.map((p) => [p, queryParams[p]]));
|
||||
req.query.endpoint = `${endpoint}?${query}`;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
req.query.endpoint = endpoint;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,8 +5,19 @@ const components = {
|
|||
bazarr: dynamic(() => import("./bazarr/component")),
|
||||
coinmarketcap: dynamic(() => import("./coinmarketcap/component")),
|
||||
overseerr: dynamic(() => import("./overseerr/component")),
|
||||
portainer: dynamic(() => import("./portainer/component")),
|
||||
prowlarr: dynamic(() => import("./prowlarr/component")),
|
||||
qbittorrent: dynamic(() => import("./qbittorrent/component")),
|
||||
radarr: dynamic(() => import("./radarr/component")),
|
||||
sonarr: dynamic(() => import("./sonarr/component")),
|
||||
readarr: dynamic(() => import("./readarr/component")),
|
||||
rutorrent: dynamic(() => import("./rutorrent/component")),
|
||||
sabnzbd: dynamic(() => import("./sabnzbd/component")),
|
||||
speedtest: dynamic(() => import("./speedtest/component")),
|
||||
strelaysrv: dynamic(() => import("./strelaysrv/component")),
|
||||
tautulli: dynamic(() => import("./tautulli/component")),
|
||||
traefik: dynamic(() => import("./traefik/component")),
|
||||
transmission: dynamic(() => import("./transmission/component")),
|
||||
};
|
||||
|
||||
export default components;
|
||||
|
|
48
src/widgets/portainer/component.jsx
Normal file
48
src/widgets/portainer/component.jsx
Normal file
|
@ -0,0 +1,48 @@
|
|||
import useSWR from "swr";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import Widget from "components/services/widgets/widget";
|
||||
import Block from "components/services/widgets/block";
|
||||
import { formatProxyUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: containersData, error: containersError } = useSWR(
|
||||
formatProxyUrl(config, `docker/containers/json`, {
|
||||
all: 1,
|
||||
})
|
||||
);
|
||||
|
||||
if (containersError) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!containersData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("portainer.running")} />
|
||||
<Block label={t("portainer.stopped")} />
|
||||
<Block label={t("portainer.total")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
if (containersData.error) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
const running = containersData.filter((c) => c.State === "running").length;
|
||||
const stopped = containersData.filter((c) => c.State === "exited").length;
|
||||
const total = containersData.length;
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("portainer.running")} value={running} />
|
||||
<Block label={t("portainer.stopped")} value={stopped} />
|
||||
<Block label={t("portainer.total")} value={total} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
15
src/widgets/portainer/widget.js
Normal file
15
src/widgets/portainer/widget.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
import genericProxyHandler from "utils/proxies/generic";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/api/endpoints/{env}/{endpoint}",
|
||||
proxyHandler: genericProxyHandler,
|
||||
|
||||
mappings: {
|
||||
"docker/containers/json": {
|
||||
endpoint: "docker/containers/json",
|
||||
params: ["all"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
54
src/widgets/prowlarr/component.jsx
Normal file
54
src/widgets/prowlarr/component.jsx
Normal file
|
@ -0,0 +1,54 @@
|
|||
import useSWR from "swr";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import Widget from "components/services/widgets/widget";
|
||||
import Block from "components/services/widgets/block";
|
||||
import { formatProxyUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: indexersData, error: indexersError } = useSWR(formatProxyUrl(config, "indexer"));
|
||||
const { data: grabsData, error: grabsError } = useSWR(formatProxyUrl(config, "indexerstats"));
|
||||
|
||||
if (indexersError || grabsError) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!indexersData || !grabsData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("prowlarr.enableIndexers")} />
|
||||
<Block label={t("prowlarr.numberOfGrabs")} />
|
||||
<Block label={t("prowlarr.numberOfQueries")} />
|
||||
<Block label={t("prowlarr.numberOfFailGrabs")} />
|
||||
<Block label={t("prowlarr.numberOfFailQueries")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
const indexers = indexersData?.filter((indexer) => indexer.enable === true);
|
||||
|
||||
let numberOfGrabs = 0;
|
||||
let numberOfQueries = 0;
|
||||
let numberOfFailedGrabs = 0;
|
||||
let numberOfFailedQueries = 0;
|
||||
grabsData?.indexers?.forEach((element) => {
|
||||
numberOfGrabs += element.numberOfGrabs;
|
||||
numberOfQueries += element.numberOfQueries;
|
||||
numberOfFailedGrabs += numberOfFailedGrabs + element.numberOfFailedGrabs;
|
||||
numberOfFailedQueries += numberOfFailedQueries + element.numberOfFailedQueries;
|
||||
});
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("prowlarr.enableIndexers")} value={indexers.length} />
|
||||
<Block label={t("prowlarr.numberOfGrabs")} value={numberOfGrabs} />
|
||||
<Block label={t("prowlarr.numberOfQueries")} value={numberOfQueries} />
|
||||
<Block label={t("prowlarr.numberOfFailGrabs")} value={numberOfFailedGrabs} />
|
||||
<Block label={t("prowlarr.numberOfFailQueries")} value={numberOfFailedQueries} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
17
src/widgets/prowlarr/widget.js
Normal file
17
src/widgets/prowlarr/widget.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
import genericProxyHandler from "utils/proxies/generic";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/api/v1/{endpoint}",
|
||||
proxyHandler: genericProxyHandler,
|
||||
|
||||
mappings: {
|
||||
indexer: {
|
||||
endpoint: "indexer",
|
||||
},
|
||||
indexerstats: {
|
||||
endpoint: "indexerstats",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
68
src/widgets/qbittorrent/component.jsx
Normal file
68
src/widgets/qbittorrent/component.jsx
Normal file
|
@ -0,0 +1,68 @@
|
|||
import useSWR from "swr";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import Widget from "components/services/widgets/widget";
|
||||
import Block from "components/services/widgets/block";
|
||||
import { formatProxyUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: torrentData, error: torrentError } = useSWR(formatProxyUrl(config, "torrents/info"));
|
||||
|
||||
if (torrentError) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!torrentData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("qbittorrent.leech")} />
|
||||
<Block label={t("qbittorrent.download")} />
|
||||
<Block label={t("qbittorrent.seed")} />
|
||||
<Block label={t("qbittorrent.upload")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
let rateDl = 0;
|
||||
let rateUl = 0;
|
||||
let completed = 0;
|
||||
|
||||
for (let i = 0; i < torrentData.length; i += 1) {
|
||||
const torrent = torrentData[i];
|
||||
rateDl += torrent.dlspeed;
|
||||
rateUl += torrent.upspeed;
|
||||
if (torrent.progress === 1) {
|
||||
completed += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const leech = torrentData.length - completed;
|
||||
|
||||
let unitsDl = "KB/s";
|
||||
let unitsUl = "KB/s";
|
||||
rateDl /= 1024;
|
||||
rateUl /= 1024;
|
||||
|
||||
if (rateDl > 1024) {
|
||||
rateDl /= 1024;
|
||||
unitsDl = "MB/s";
|
||||
}
|
||||
|
||||
if (rateUl > 1024) {
|
||||
rateUl /= 1024;
|
||||
unitsUl = "MB/s";
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("qbittorrent.leech")} value={t("common.number", { value: leech })} />
|
||||
<Block label={t("qbittorrent.download")} value={`${rateDl.toFixed(2)} ${unitsDl}`} />
|
||||
<Block label={t("qbittorrent.seed")} value={t("common.number", { value: completed })} />
|
||||
<Block label={t("qbittorrent.upload")} value={`${rateUl.toFixed(2)} ${unitsUl}`} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
8
src/widgets/qbittorrent/widget.js
Normal file
8
src/widgets/qbittorrent/widget.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
import qbittorrentProxyHandler from "./proxy";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/api/v2/{endpoint}",
|
||||
proxyHandler: qbittorrentProxyHandler,
|
||||
};
|
||||
|
||||
export default widget;
|
38
src/widgets/readarr/component.jsx
Normal file
38
src/widgets/readarr/component.jsx
Normal file
|
@ -0,0 +1,38 @@
|
|||
import useSWR from "swr";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import Widget from "components/services/widgets/widget";
|
||||
import Block from "components/services/widgets/block";
|
||||
import { formatProxyUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: booksData, error: booksError } = useSWR(formatProxyUrl(config, "book"));
|
||||
const { data: wantedData, error: wantedError } = useSWR(formatProxyUrl(config, "wanted/missing"));
|
||||
const { data: queueData, error: queueError } = useSWR(formatProxyUrl(config, "queue/status"));
|
||||
|
||||
if (booksError || wantedError || queueError) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!booksData || !wantedData || !queueData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("readarr.wanted")} />
|
||||
<Block label={t("readarr.queued")} />
|
||||
<Block label={t("readarr.books")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("readarr.wanted")} value={t("common.number", { value: wantedData.totalRecords })} />
|
||||
<Block label={t("readarr.queued")} value={t("common.number", { value: queueData.totalCount })} />
|
||||
<Block label={t("readarr.books")} value={t("common.number", { value: booksData.have })} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
24
src/widgets/readarr/widget.js
Normal file
24
src/widgets/readarr/widget.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import genericProxyHandler from "utils/proxies/generic";
|
||||
import { jsonArrayFilter } from "utils/api-helpers";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/api/v1/{endpoint}?apikey={key}",
|
||||
proxyHandler: genericProxyHandler,
|
||||
|
||||
mappings: {
|
||||
book: {
|
||||
endpoint: "book",
|
||||
map: (data) => ({
|
||||
have: jsonArrayFilter(data, (item) => item?.statistics?.bookFileCount > 0).length,
|
||||
}),
|
||||
},
|
||||
"queue/status": {
|
||||
endpoint: "queue/status",
|
||||
},
|
||||
"wanted/missing": {
|
||||
endpoint: "wanted/missing",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
42
src/widgets/rutorrent/component.jsx
Normal file
42
src/widgets/rutorrent/component.jsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
import useSWR from "swr";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import Widget from "components/services/widgets/widget";
|
||||
import Block from "components/services/widgets/block";
|
||||
import { formatProxyUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: statusData, error: statusError } = useSWR(formatProxyUrl(config));
|
||||
|
||||
if (statusError) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!statusData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("rutorrent.active")} />
|
||||
<Block label={t("rutorrent.upload")} />
|
||||
<Block label={t("rutorrent.download")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
const upload = statusData.reduce((acc, torrent) => acc + parseInt(torrent["d.get_up_rate"], 10), 0);
|
||||
|
||||
const download = statusData.reduce((acc, torrent) => acc + parseInt(torrent["d.get_down_rate"], 10), 0);
|
||||
|
||||
const active = statusData.filter((torrent) => torrent["d.get_state"] === "1");
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("rutorrent.active")} value={active.length} />
|
||||
<Block label={t("rutorrent.upload")} value={t("common.bitrate", { value: upload })} />
|
||||
<Block label={t("rutorrent.download")} value={t("common.bitrate", { value: download })} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
8
src/widgets/rutorrent/widget.js
Normal file
8
src/widgets/rutorrent/widget.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
import rutorrentProxyHandler from "./proxy";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/plugins/httprpc/action.php",
|
||||
proxyHandler: rutorrentProxyHandler,
|
||||
};
|
||||
|
||||
export default widget;
|
36
src/widgets/sabnzbd/component.jsx
Normal file
36
src/widgets/sabnzbd/component.jsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
import useSWR from "swr";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import Widget from "components/services/widgets/widget";
|
||||
import Block from "components/services/widgets/block";
|
||||
import { formatProxyUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: queueData, error: queueError } = useSWR(formatProxyUrl(config, "queue"));
|
||||
|
||||
if (queueError) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!queueData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("sabnzbd.rate")} />
|
||||
<Block label={t("sabnzbd.queue")} />
|
||||
<Block label={t("sabnzbd.timeleft")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("sabnzbd.rate")} value={`${queueData.queue.speed}B/s`} />
|
||||
<Block label={t("sabnzbd.queue")} value={t("common.number", { value: queueData.queue.noofslots })} />
|
||||
<Block label={t("sabnzbd.timeleft")} value={queueData.queue.timeleft} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
14
src/widgets/sabnzbd/widget.js
Normal file
14
src/widgets/sabnzbd/widget.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
import genericProxyHandler from "utils/proxies/generic";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/api/?apikey={key}&output=json&mode={endpoint}",
|
||||
proxyHandler: genericProxyHandler,
|
||||
|
||||
mappings: {
|
||||
queue: {
|
||||
endpoint: "queue",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
|
@ -6,19 +6,19 @@ const widget = {
|
|||
proxyHandler: genericProxyHandler,
|
||||
|
||||
mappings: {
|
||||
"series": {
|
||||
series: {
|
||||
endpoint: "series",
|
||||
map: (data) => ({
|
||||
total: asJson(data).length,
|
||||
}),
|
||||
},
|
||||
"queue": {
|
||||
queue: {
|
||||
endpoint: "queue",
|
||||
},
|
||||
"wanted/missing": {
|
||||
endpoint: "wanted/missing",
|
||||
},
|
||||
endpoint: "wanted/missing",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
||||
|
|
45
src/widgets/speedtest/component.jsx
Normal file
45
src/widgets/speedtest/component.jsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
import useSWR from "swr";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import Widget from "components/services/widgets/widget";
|
||||
import Block from "components/services/widgets/block";
|
||||
import { formatProxyUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: speedtestData, error: speedtestError } = useSWR(formatProxyUrl(config, "speedtest/latest"));
|
||||
|
||||
if (speedtestError) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!speedtestData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("speedtest.download")} />
|
||||
<Block label={t("speedtest.upload")} />
|
||||
<Block label={t("speedtest.ping")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block
|
||||
label={t("speedtest.download")}
|
||||
value={t("common.bitrate", { value: speedtestData.data.download * 1024 * 1024 })}
|
||||
/>
|
||||
<Block
|
||||
label={t("speedtest.upload")}
|
||||
value={t("common.bitrate", { value: speedtestData.data.upload * 1024 * 1024 })}
|
||||
/>
|
||||
<Block
|
||||
label={t("speedtest.ping")}
|
||||
value={t("common.ms", { value: speedtestData.data.ping, style: "unit", unit: "millisecond" })}
|
||||
/>
|
||||
</Widget>
|
||||
);
|
||||
}
|
14
src/widgets/speedtest/widget.js
Normal file
14
src/widgets/speedtest/widget.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
import genericProxyHandler from "utils/proxies/generic";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/api/{endpoint}",
|
||||
proxyHandler: genericProxyHandler,
|
||||
|
||||
mappings: {
|
||||
"speedtest/latest": {
|
||||
endpoint: "speedtest/latest",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
43
src/widgets/strelaysrv/component.jsx
Normal file
43
src/widgets/strelaysrv/component.jsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
import useSWR from "swr";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import Widget from "components/services/widgets/widget";
|
||||
import Block from "components/services/widgets/block";
|
||||
import { formatProxyUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: statsData, error: statsError } = useSWR(formatProxyUrl(config, `status`));
|
||||
|
||||
if (statsError) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!statsData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("strelaysrv.numActiveSessions")} />
|
||||
<Block label={t("strelaysrv.numConnections")} />
|
||||
<Block label={t("strelaysrv.bytesProxied")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block
|
||||
label={t("strelaysrv.numActiveSessions")}
|
||||
value={t("common.number", { value: statsData.numActiveSessions })}
|
||||
/>
|
||||
<Block label={t("strelaysrv.numConnections")} value={t("common.number", { value: statsData.numConnections })} />
|
||||
<Block label={t("strelaysrv.dataRelayed")} value={t("common.bytes", { value: statsData.bytesProxied })} />
|
||||
<Block
|
||||
label={t("strelaysrv.transferRate")}
|
||||
value={t("common.bitrate", { value: statsData.kbps10s1m5m15m30m60m[5] })}
|
||||
/>
|
||||
</Widget>
|
||||
);
|
||||
}
|
14
src/widgets/strelaysrv/widget.js
Normal file
14
src/widgets/strelaysrv/widget.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
import genericProxyHandler from "utils/proxies/generic";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/{endpoint}",
|
||||
proxyHandler: genericProxyHandler,
|
||||
|
||||
mappings: {
|
||||
status: {
|
||||
endpoint: "status",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
182
src/widgets/tautulli/component.jsx
Normal file
182
src/widgets/tautulli/component.jsx
Normal file
|
@ -0,0 +1,182 @@
|
|||
/* eslint-disable camelcase */
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { BsFillPlayFill, BsPauseFill, BsCpu, BsFillCpuFill } from "react-icons/bs";
|
||||
import { MdOutlineSmartDisplay, MdSmartDisplay } from "react-icons/md";
|
||||
|
||||
import Widget from "components/services/widgets/widget";
|
||||
import { formatProxyUrl } from "utils/api-helpers";
|
||||
|
||||
function millisecondsToTime(milliseconds) {
|
||||
const seconds = Math.floor((milliseconds / 1000) % 60);
|
||||
const minutes = Math.floor((milliseconds / (1000 * 60)) % 60);
|
||||
const hours = Math.floor((milliseconds / (1000 * 60 * 60)) % 24);
|
||||
return { hours, minutes, seconds };
|
||||
}
|
||||
|
||||
function millisecondsToString(milliseconds) {
|
||||
const { hours, minutes, seconds } = millisecondsToTime(milliseconds);
|
||||
const parts = [];
|
||||
if (hours > 0) {
|
||||
parts.push(hours);
|
||||
}
|
||||
parts.push(minutes);
|
||||
parts.push(seconds);
|
||||
|
||||
return parts.map((part) => part.toString().padStart(2, "0")).join(":");
|
||||
}
|
||||
|
||||
function SingleSessionEntry({ session }) {
|
||||
const { full_title, duration, view_offset, progress_percent, state, video_decision, audio_decision } = session;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="text-theme-700 dark:text-theme-200 relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1 flex">
|
||||
<div className="text-xs z-10 self-center ml-2 relative w-full h-4 grow mr-2">
|
||||
<div className="absolute w-full whitespace-nowrap text-ellipsis overflow-hidden">{full_title}</div>
|
||||
</div>
|
||||
<div className="self-center text-xs flex justify-end mr-1.5 pl-1">
|
||||
{video_decision === "direct play" && audio_decision === "direct play" && (
|
||||
<MdSmartDisplay className="opacity-50" />
|
||||
)}
|
||||
{video_decision === "copy" && audio_decision === "copy" && <MdOutlineSmartDisplay className="opacity-50" />}
|
||||
{video_decision !== "copy" &&
|
||||
video_decision !== "direct play" &&
|
||||
(audio_decision !== "copy" || audio_decision !== "direct play") && <BsFillCpuFill className="opacity-50" />}
|
||||
{(video_decision === "copy" || video_decision === "direct play") &&
|
||||
audio_decision !== "copy" &&
|
||||
audio_decision !== "direct play" && <BsCpu className="opacity-50" />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-theme-700 dark:text-theme-200 relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1 flex">
|
||||
<div
|
||||
className="absolute h-5 rounded-md bg-theme-200 dark:bg-theme-900/40 z-0"
|
||||
style={{
|
||||
width: `${progress_percent}%`,
|
||||
}}
|
||||
/>
|
||||
<div className="text-xs z-10 self-center ml-1">
|
||||
{state === "paused" && (
|
||||
<BsPauseFill className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80" />
|
||||
)}
|
||||
{state !== "paused" && (
|
||||
<BsFillPlayFill className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80" />
|
||||
)}
|
||||
</div>
|
||||
<div className="grow " />
|
||||
<div className="self-center text-xs flex justify-end mr-2 z-10">
|
||||
{millisecondsToString(view_offset)}
|
||||
<span className="mx-0.5 text-[8px]">/</span>
|
||||
{millisecondsToString(duration)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SessionEntry({ session }) {
|
||||
const { full_title, view_offset, progress_percent, state, video_decision, audio_decision } = session;
|
||||
|
||||
return (
|
||||
<div className="text-theme-700 dark:text-theme-200 relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1 flex">
|
||||
<div
|
||||
className="absolute h-5 rounded-md bg-theme-200 dark:bg-theme-900/40 z-0"
|
||||
style={{
|
||||
width: `${progress_percent}%`,
|
||||
}}
|
||||
/>
|
||||
<div className="text-xs z-10 self-center ml-1">
|
||||
{state === "paused" && (
|
||||
<BsPauseFill className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80" />
|
||||
)}
|
||||
{state !== "paused" && (
|
||||
<BsFillPlayFill className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80" />
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs z-10 self-center ml-2 relative w-full h-4 grow mr-2">
|
||||
<div className="absolute w-full whitespace-nowrap text-ellipsis overflow-hidden">{full_title}</div>
|
||||
</div>
|
||||
<div className="self-center text-xs flex justify-end mr-1.5 pl-1 z-10">
|
||||
{video_decision === "direct play" && audio_decision === "direct play" && (
|
||||
<MdSmartDisplay className="opacity-50" />
|
||||
)}
|
||||
{video_decision === "copy" && audio_decision === "copy" && <MdOutlineSmartDisplay className="opacity-50" />}
|
||||
{video_decision !== "copy" &&
|
||||
video_decision !== "direct play" &&
|
||||
(audio_decision !== "copy" || audio_decision !== "direct play") && <BsFillCpuFill className="opacity-50" />}
|
||||
{(video_decision === "copy" || video_decision === "direct play") &&
|
||||
audio_decision !== "copy" &&
|
||||
audio_decision !== "direct play" && <BsCpu className="opacity-50" />}
|
||||
</div>
|
||||
<div className="self-center text-xs flex justify-end mr-2 z-10">{millisecondsToString(view_offset)}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: activityData, error: activityError } = useSWR(formatProxyUrl(config, "get_activity"), {
|
||||
refreshInterval: 5000,
|
||||
});
|
||||
|
||||
if (activityError) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!activityData) {
|
||||
return (
|
||||
<div className="flex flex-col pb-1 mx-1">
|
||||
<div className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1">
|
||||
<span className="absolute left-2 text-xs mt-[2px]">-</span>
|
||||
</div>
|
||||
<div className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1">
|
||||
<span className="absolute left-2 text-xs mt-[2px]">-</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const playing = activityData.response.data.sessions.sort((a, b) => {
|
||||
if (a.view_offset > b.view_offset) {
|
||||
return 1;
|
||||
}
|
||||
if (a.view_offset < b.view_offset) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
if (playing.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col pb-1 mx-1">
|
||||
<div className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1">
|
||||
<span className="absolute left-2 text-xs mt-[2px]">{t("tautulli.no_active")}</span>
|
||||
</div>
|
||||
<div className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1">
|
||||
<span className="absolute left-2 text-xs mt-[2px]">-</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (playing.length === 1) {
|
||||
const session = playing[0];
|
||||
return (
|
||||
<div className="flex flex-col pb-1 mx-1">
|
||||
<SingleSessionEntry session={session} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col pb-1 mx-1">
|
||||
{playing.map((session) => (
|
||||
<SessionEntry key={session.Id} session={session} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
14
src/widgets/tautulli/widget.js
Normal file
14
src/widgets/tautulli/widget.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
import genericProxyHandler from "utils/proxies/generic";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/api/v2?apikey={key}&cmd={endpoint}",
|
||||
proxyHandler: genericProxyHandler,
|
||||
|
||||
mappings: {
|
||||
get_activity: {
|
||||
endpoint: "get_activity",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
36
src/widgets/traefik/component.jsx
Normal file
36
src/widgets/traefik/component.jsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
import useSWR from "swr";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import Widget from "components/services/widgets/widget";
|
||||
import Block from "components/services/widgets/block";
|
||||
import { formatProxyUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: traefikData, error: traefikError } = useSWR(formatProxyUrl(config, "overview"));
|
||||
|
||||
if (traefikError) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!traefikData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("traefik.routers")} />
|
||||
<Block label={t("traefik.services")} />
|
||||
<Block label={t("traefik.middleware")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("traefik.routers")} value={traefikData.http.routers.total} />
|
||||
<Block label={t("traefik.services")} value={traefikData.http.services.total} />
|
||||
<Block label={t("traefik.middleware")} value={traefikData.http.middlewares.total} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
14
src/widgets/traefik/widget.js
Normal file
14
src/widgets/traefik/widget.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
import genericProxyHandler from "utils/proxies/generic";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/api/{endpoint}",
|
||||
proxyHandler: genericProxyHandler,
|
||||
|
||||
mappings: {
|
||||
overview: {
|
||||
endpoint: "overview",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
69
src/widgets/transmission/component.jsx
Normal file
69
src/widgets/transmission/component.jsx
Normal file
|
@ -0,0 +1,69 @@
|
|||
import useSWR from "swr";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import Widget from "components/services/widgets/widget";
|
||||
import Block from "components/services/widgets/block";
|
||||
import { formatProxyUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: torrentData, error: torrentError } = useSWR(formatProxyUrl(config));
|
||||
|
||||
if (torrentError) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!torrentData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("transmission.leech")} />
|
||||
<Block label={t("transmission.download")} />
|
||||
<Block label={t("transmission.seed")} />
|
||||
<Block label={t("transmission.upload")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
const { torrents } = torrentData.arguments;
|
||||
let rateDl = 0;
|
||||
let rateUl = 0;
|
||||
let completed = 0;
|
||||
|
||||
for (let i = 0; i < torrents.length; i += 1) {
|
||||
const torrent = torrents[i];
|
||||
rateDl += torrent.rateDownload;
|
||||
rateUl += torrent.rateUpload;
|
||||
if (torrent.percentDone === 1) {
|
||||
completed += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const leech = torrents.length - completed;
|
||||
|
||||
let unitsDl = "KB/s";
|
||||
let unitsUl = "KB/s";
|
||||
rateDl /= 1024;
|
||||
rateUl /= 1024;
|
||||
|
||||
if (rateDl > 1024) {
|
||||
rateDl /= 1024;
|
||||
unitsDl = "MB/s";
|
||||
}
|
||||
|
||||
if (rateUl > 1024) {
|
||||
rateUl /= 1024;
|
||||
unitsUl = "MB/s";
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("transmission.leech")} value={t("common.number", { value: leech })} />
|
||||
<Block label={t("transmission.download")} value={`${rateDl.toFixed(2)} ${unitsDl}`} />
|
||||
<Block label={t("transmission.seed")} value={t("common.number", { value: completed })} />
|
||||
<Block label={t("transmission.upload")} value={`${rateUl.toFixed(2)} ${unitsUl}`} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
8
src/widgets/transmission/widget.js
Normal file
8
src/widgets/transmission/widget.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
import transmissionProxyHandler from "./proxy";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/transmission/rpc",
|
||||
proxyHandler: transmissionProxyHandler,
|
||||
};
|
||||
|
||||
export default widget;
|
|
@ -2,16 +2,38 @@ import adguard from "./adguard/widget";
|
|||
import bazarr from "./bazarr/widget";
|
||||
import coinmarketcap from "./coinmarketcap/widget";
|
||||
import overseerr from "./overseerr/widget";
|
||||
import portainer from "./portainer/widget";
|
||||
import prowlarr from "./prowlarr/widget";
|
||||
import qbittorrent from "./qbittorrent/widget";
|
||||
import radarr from "./radarr/widget";
|
||||
import sonarr from "./sonarr/widget"
|
||||
import sonarr from "./sonarr/widget";
|
||||
import readarr from "./readarr/widget";
|
||||
import rutorrent from "./rutorrent/widget";
|
||||
import sabnzbd from "./sabnzbd/widget";
|
||||
import speedtest from "./speedtest/widget";
|
||||
import strelaysrv from "./strelaysrv/widget";
|
||||
import tautulli from "./tautulli/widget";
|
||||
import traefik from "./traefik/widget";
|
||||
import transmission from "./transmission/widget";
|
||||
|
||||
const widgets = {
|
||||
adguard,
|
||||
bazarr,
|
||||
coinmarketcap,
|
||||
overseerr,
|
||||
portainer,
|
||||
prowlarr,
|
||||
qbittorrent,
|
||||
radarr,
|
||||
sonarr,
|
||||
readarr,
|
||||
rutorrent,
|
||||
sabnzbd,
|
||||
speedtest,
|
||||
strelaysrv,
|
||||
tautulli,
|
||||
traefik,
|
||||
transmission,
|
||||
};
|
||||
|
||||
export default widgets;
|
||||
|
|
Loading…
Add table
Reference in a new issue