diff --git a/.eslintrc.json b/.eslintrc.json index fc0b1b8c6..6c1da17dd 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -2,6 +2,12 @@ "extends": ["airbnb", "next/core-web-vitals", "prettier"], "plugins": ["prettier"], "rules": { + "import/no-cycle": [ + "error", + { + "maxDepth": 1 + } + ], "import/order": [ "error", { diff --git a/src/components/services/widget.jsx b/src/components/services/widget.jsx index dd65b482f..f6ae42044 100644 --- a/src/components/services/widget.jsx +++ b/src/components/services/widget.jsx @@ -1,72 +1,11 @@ -import dynamic from "next/dynamic"; import { useTranslation } from "next-i18next"; -const Sonarr = dynamic(() => import("./widgets/service/sonarr")); -const Radarr = dynamic(() => import("./widgets/service/radarr")); -const Lidarr = dynamic(() => import("./widgets/service/lidarr")); -const Readarr = dynamic(() => import("./widgets/service/readarr")); -const Bazarr = dynamic(() => import("./widgets/service/bazarr")); -const Ombi = dynamic(() => import("./widgets/service/ombi")); -const Portainer = dynamic(() => import("./widgets/service/portainer")); -const Emby = dynamic(() => import("./widgets/service/emby")); -const Nzbget = dynamic(() => import("./widgets/service/nzbget")); -const SABnzbd = dynamic(() => import("./widgets/service/sabnzbd")); -const Transmission = dynamic(() => import("./widgets/service/transmission")); -const QBittorrent = dynamic(() => import("./widgets/service/qbittorrent")); -const Docker = dynamic(() => import("./widgets/service/docker")); -const Pihole = dynamic(() => import("./widgets/service/pihole")); -const Rutorrent = dynamic(() => import("./widgets/service/rutorrent")); -const Jellyfin = dynamic(() => import("./widgets/service/jellyfin")); -const Speedtest = dynamic(() => import("./widgets/service/speedtest")); -const Traefik = dynamic(() => import("./widgets/service/traefik")); -const Jellyseerr = dynamic(() => import("./widgets/service/jellyseerr")); -const Overseerr = dynamic(() => import("./widgets/service/overseerr")); -const Npm = dynamic(() => import("./widgets/service/npm")); -const Tautulli = dynamic(() => import("./widgets/service/tautulli")); -const CoinMarketCap = dynamic(() => import("./widgets/service/coinmarketcap")); -const Gotify = dynamic(() => import("./widgets/service/gotify")); -const Prowlarr = dynamic(() => import("./widgets/service/prowlarr")); -const Jackett = dynamic(() => import("./widgets/service/jackett")); -const AdGuard = dynamic(() => import("./widgets/service/adguard")); -const StRelaySrv = dynamic(() => import("./widgets/service/strelaysrv")); -const Mastodon = dynamic(() => import("./widgets/service/mastodon")); - -const widgetMappings = { - docker: Docker, - sonarr: Sonarr, - radarr: Radarr, - lidarr: Lidarr, - readarr: Readarr, - bazarr: Bazarr, - ombi: Ombi, - portainer: Portainer, - emby: Emby, - jellyfin: Jellyfin, - nzbget: Nzbget, - sabnzbd: SABnzbd, - transmission: Transmission, - qbittorrent: QBittorrent, - pihole: Pihole, - rutorrent: Rutorrent, - speedtest: Speedtest, - traefik: Traefik, - jellyseerr: Jellyseerr, - overseerr: Overseerr, - coinmarketcap: CoinMarketCap, - npm: Npm, - tautulli: Tautulli, - gotify: Gotify, - prowlarr: Prowlarr, - jackett: Jackett, - adguard: AdGuard, - strelaysrv: StRelaySrv, - mastodon: Mastodon, -}; +import components from "widgets/components"; export default function Widget({ service }) { const { t } = useTranslation("common"); - const ServiceWidget = widgetMappings[service.widget.type]; + const ServiceWidget = components[service.widget.type]; if (ServiceWidget) { return ; diff --git a/src/components/services/widgets/service/adguard.jsx b/src/components/services/widgets/service/adguard.jsx index a3ad75bbe..1befec86d 100644 --- a/src/components/services/widgets/service/adguard.jsx +++ b/src/components/services/widgets/service/adguard.jsx @@ -4,14 +4,14 @@ import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; -import { formatApiUrl } from "utils/api-helpers"; +import { formatProxyUrl } from "utils/api-helpers"; export default function AdGuard({ service }) { const { t } = useTranslation(); const config = service.widget; - const { data: adguardData, error: adguardError } = useSWR(formatApiUrl(config, "stats")); + const { data: adguardData, error: adguardError } = useSWR(formatProxyUrl(config, "stats")); if (adguardError) { return ; diff --git a/src/components/services/widgets/service/bazarr.jsx b/src/components/services/widgets/service/bazarr.jsx index ef2c6cca1..33b4defcb 100644 --- a/src/components/services/widgets/service/bazarr.jsx +++ b/src/components/services/widgets/service/bazarr.jsx @@ -4,15 +4,15 @@ import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; -import { formatApiUrl } from "utils/api-helpers"; +import { formatProxyUrl } from "utils/api-helpers"; export default function Bazarr({ service }) { const { t } = useTranslation(); const config = service.widget; - const { data: episodesData, error: episodesError } = useSWR(formatApiUrl(config, "episodes")); - const { data: moviesData, error: moviesError } = useSWR(formatApiUrl(config, "movies")); + const { data: episodesData, error: episodesError } = useSWR(formatProxyUrl(config, "episodes")); + const { data: moviesData, error: moviesError } = useSWR(formatProxyUrl(config, "movies")); if (episodesError || moviesError) { return ; diff --git a/src/components/services/widgets/service/coinmarketcap.jsx b/src/components/services/widgets/service/coinmarketcap.jsx index 32b04713f..d775e3fa1 100644 --- a/src/components/services/widgets/service/coinmarketcap.jsx +++ b/src/components/services/widgets/service/coinmarketcap.jsx @@ -7,7 +7,7 @@ import Widget from "../widget"; import Block from "../block"; import Dropdown from "components/services/dropdown"; -import { formatApiUrl } from "utils/api-helpers"; +import { formatProxyUrl } from "utils/api-helpers"; export default function CoinMarketCap({ service }) { const { t } = useTranslation(); @@ -26,7 +26,7 @@ export default function CoinMarketCap({ service }) { const { symbols } = config; const { data: statsData, error: statsError } = useSWR( - formatApiUrl(config, `v1/cryptocurrency/quotes/latest?symbol=${symbols.join(",")}&convert=${currencyCode}`) + formatProxyUrl(config, `v1/cryptocurrency/quotes/latest?symbol=${symbols.join(",")}&convert=${currencyCode}`) ); if (!symbols || symbols.length === 0) { diff --git a/src/components/services/widgets/service/emby.jsx b/src/components/services/widgets/service/emby.jsx index 63ae9c8e3..46ea129f5 100644 --- a/src/components/services/widgets/service/emby.jsx +++ b/src/components/services/widgets/service/emby.jsx @@ -5,7 +5,7 @@ import { MdOutlineSmartDisplay } from "react-icons/md"; import Widget from "../widget"; -import { formatApiUrl } from "utils/api-helpers"; +import { formatProxyUrl } from "utils/api-helpers"; function ticksToTime(ticks) { const milliseconds = ticks / 10000; @@ -158,12 +158,12 @@ export default function Emby({ service }) { data: sessionsData, error: sessionsError, mutate: sessionMutate, - } = useSWR(formatApiUrl(config, "Sessions"), { + } = useSWR(formatProxyUrl(config, "Sessions"), { refreshInterval: 5000, }); async function handlePlayCommand(session, command) { - const url = formatApiUrl(config, `Sessions/${session.Id}/Playing/${command}`); + const url = formatProxyUrl(config, `Sessions/${session.Id}/Playing/${command}`); await fetch(url, { method: "POST", }).then(() => { diff --git a/src/components/services/widgets/service/gotify.jsx b/src/components/services/widgets/service/gotify.jsx index 4bd9f98b9..292155749 100644 --- a/src/components/services/widgets/service/gotify.jsx +++ b/src/components/services/widgets/service/gotify.jsx @@ -4,16 +4,16 @@ import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; -import { formatApiUrl } from "utils/api-helpers"; +import { formatProxyUrl } from "utils/api-helpers"; export default function Gotify({ service }) { const { t } = useTranslation(); const config = service.widget; - const { data: appsData, error: appsError } = useSWR(formatApiUrl(config, `application`)); - const { data: messagesData, error: messagesError } = useSWR(formatApiUrl(config, `message`)); - const { data: clientsData, error: clientsError } = useSWR(formatApiUrl(config, `client`)); + const { data: appsData, error: appsError } = useSWR(formatProxyUrl(config, `application`)); + const { data: messagesData, error: messagesError } = useSWR(formatProxyUrl(config, `message`)); + const { data: clientsData, error: clientsError } = useSWR(formatProxyUrl(config, `client`)); if (appsError || messagesError || clientsError) { return ; diff --git a/src/components/services/widgets/service/jackett.jsx b/src/components/services/widgets/service/jackett.jsx index 70de6c074..f9cc2dfb2 100644 --- a/src/components/services/widgets/service/jackett.jsx +++ b/src/components/services/widgets/service/jackett.jsx @@ -4,14 +4,14 @@ import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; -import { formatApiUrl } from "utils/api-helpers"; +import { formatProxyUrl } from "utils/api-helpers"; export default function Jackett({ service }) { const { t } = useTranslation(); const config = service.widget; - const { data: indexersData, error: indexersError } = useSWR(formatApiUrl(config, "indexers")); + const { data: indexersData, error: indexersError } = useSWR(formatProxyUrl(config, "indexers")); if (indexersError) { return ; diff --git a/src/components/services/widgets/service/jellyseerr.jsx b/src/components/services/widgets/service/jellyseerr.jsx index 9bcc1e278..f4d5b71e0 100644 --- a/src/components/services/widgets/service/jellyseerr.jsx +++ b/src/components/services/widgets/service/jellyseerr.jsx @@ -4,14 +4,14 @@ import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; -import { formatApiUrl } from "utils/api-helpers"; +import { formatProxyUrl } from "utils/api-helpers"; export default function Jellyseerr({ service }) { const { t } = useTranslation(); const config = service.widget; - const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, `request/count`)); + const { data: statsData, error: statsError } = useSWR(formatProxyUrl(config, `request/count`)); if (statsError) { return ; diff --git a/src/components/services/widgets/service/lidarr.jsx b/src/components/services/widgets/service/lidarr.jsx index 2bf4cddb5..4b985aee3 100644 --- a/src/components/services/widgets/service/lidarr.jsx +++ b/src/components/services/widgets/service/lidarr.jsx @@ -4,16 +4,16 @@ import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; -import { formatApiUrl } from "utils/api-helpers"; +import { formatProxyUrl } from "utils/api-helpers"; export default function Lidarr({ service }) { const { t } = useTranslation(); const config = service.widget; - const { data: albumsData, error: albumsError } = useSWR(formatApiUrl(config, "album")); - const { data: wantedData, error: wantedError } = useSWR(formatApiUrl(config, "wanted/missing")); - const { data: queueData, error: queueError } = useSWR(formatApiUrl(config, "queue/status")); + const { data: albumsData, error: albumsError } = useSWR(formatProxyUrl(config, "album")); + const { data: wantedData, error: wantedError } = useSWR(formatProxyUrl(config, "wanted/missing")); + const { data: queueData, error: queueError } = useSWR(formatProxyUrl(config, "queue/status")); if (albumsError || wantedError || queueError) { return ; diff --git a/src/components/services/widgets/service/mastodon.jsx b/src/components/services/widgets/service/mastodon.jsx index 20b14aca2..d1bb22527 100644 --- a/src/components/services/widgets/service/mastodon.jsx +++ b/src/components/services/widgets/service/mastodon.jsx @@ -4,14 +4,14 @@ import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; -import { formatApiUrl } from "utils/api-helpers"; +import { formatProxyUrl } from "utils/api-helpers"; export default function Mastodon({ service }) { const { t } = useTranslation(); const config = service.widget; - const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, `instance`)); + const { data: statsData, error: statsError } = useSWR(formatProxyUrl(config, `instance`)); if (statsError) { return ; @@ -29,7 +29,7 @@ export default function Mastodon({ service }) { return ( - + diff --git a/src/components/services/widgets/service/npm.jsx b/src/components/services/widgets/service/npm.jsx index 563348c2c..93ecf26bd 100644 --- a/src/components/services/widgets/service/npm.jsx +++ b/src/components/services/widgets/service/npm.jsx @@ -4,14 +4,14 @@ import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; -import { formatApiUrl } from "utils/api-helpers"; +import { formatProxyUrl } from "utils/api-helpers"; export default function Npm({ service }) { const { t } = useTranslation(); const config = service.widget; - const { data: infoData, error: infoError } = useSWR(formatApiUrl(config, "nginx/proxy-hosts")); + const { data: infoData, error: infoError } = useSWR(formatProxyUrl(config, "nginx/proxy-hosts")); if (infoError) { return ; diff --git a/src/components/services/widgets/service/nzbget.jsx b/src/components/services/widgets/service/nzbget.jsx index b81258438..58d088503 100644 --- a/src/components/services/widgets/service/nzbget.jsx +++ b/src/components/services/widgets/service/nzbget.jsx @@ -4,14 +4,14 @@ import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; -import { formatApiUrl } from "utils/api-helpers"; +import { formatProxyUrl } from "utils/api-helpers"; export default function Nzbget({ service }) { const { t } = useTranslation("common"); const config = service.widget; - const { data: statusData, error: statusError } = useSWR(formatApiUrl(config, "status")); + const { data: statusData, error: statusError } = useSWR(formatProxyUrl(config, "status")); if (statusError) { return ; diff --git a/src/components/services/widgets/service/ombi.jsx b/src/components/services/widgets/service/ombi.jsx index aa1a5f652..887c7348c 100644 --- a/src/components/services/widgets/service/ombi.jsx +++ b/src/components/services/widgets/service/ombi.jsx @@ -4,14 +4,14 @@ import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; -import { formatApiUrl } from "utils/api-helpers"; +import { formatProxyUrl } from "utils/api-helpers"; export default function Ombi({ service }) { const { t } = useTranslation(); const config = service.widget; - const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, `Request/count`)); + const { data: statsData, error: statsError } = useSWR(formatProxyUrl(config, `Request/count`)); if (statsError) { return ; diff --git a/src/components/services/widgets/service/overseerr.jsx b/src/components/services/widgets/service/overseerr.jsx index 59644e0a2..834172361 100644 --- a/src/components/services/widgets/service/overseerr.jsx +++ b/src/components/services/widgets/service/overseerr.jsx @@ -4,14 +4,14 @@ import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; -import { formatApiUrl } from "utils/api-helpers"; +import { formatProxyUrl } from "utils/api-helpers"; export default function Overseerr({ service }) { const { t } = useTranslation(); const config = service.widget; - const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, `request/count`)); + const { data: statsData, error: statsError } = useSWR(formatProxyUrl(config, `request/count`)); if (statsError) { return ; diff --git a/src/components/services/widgets/service/pihole.jsx b/src/components/services/widgets/service/pihole.jsx index 3468f5fc9..720cd681e 100644 --- a/src/components/services/widgets/service/pihole.jsx +++ b/src/components/services/widgets/service/pihole.jsx @@ -4,14 +4,14 @@ import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; -import { formatApiUrl } from "utils/api-helpers"; +import { formatProxyUrl } from "utils/api-helpers"; export default function Pihole({ service }) { const { t } = useTranslation(); const config = service.widget; - const { data: piholeData, error: piholeError } = useSWR(formatApiUrl(config, "api.php")); + const { data: piholeData, error: piholeError } = useSWR(formatProxyUrl(config, "api.php")); if (piholeError) { return ; diff --git a/src/components/services/widgets/service/portainer.jsx b/src/components/services/widgets/service/portainer.jsx index ab1c68951..1c28333be 100644 --- a/src/components/services/widgets/service/portainer.jsx +++ b/src/components/services/widgets/service/portainer.jsx @@ -4,14 +4,16 @@ import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; -import { formatApiUrl } from "utils/api-helpers"; +import { formatProxyUrl } from "utils/api-helpers"; export default function Portainer({ service }) { const { t } = useTranslation(); const config = service.widget; - const { data: containersData, error: containersError } = useSWR(formatApiUrl(config, `docker/containers/json?all=1`)); + const { data: containersData, error: containersError } = useSWR( + formatProxyUrl(config, `docker/containers/json?all=1`) + ); if (containersError) { return ; diff --git a/src/components/services/widgets/service/prowlarr.jsx b/src/components/services/widgets/service/prowlarr.jsx index b46eb7a31..e80b9e3d6 100644 --- a/src/components/services/widgets/service/prowlarr.jsx +++ b/src/components/services/widgets/service/prowlarr.jsx @@ -4,16 +4,16 @@ import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; -import { formatApiUrl } from "utils/api-helpers"; +import { formatProxyUrl } from "utils/api-helpers"; export default function Prowlarr({ service }) { const { t } = useTranslation(); const config = service.widget; - const { data: indexersData, error: indexersError } = useSWR(formatApiUrl(config, "indexer")); - const { data: grabsData, error: grabsError } = useSWR(formatApiUrl(config, "indexerstats")); - + const { data: indexersData, error: indexersError } = useSWR(formatProxyUrl(config, "indexer")); + const { data: grabsData, error: grabsError } = useSWR(formatProxyUrl(config, "indexerstats")); + if (indexersError || grabsError) { return ; } @@ -32,11 +32,11 @@ export default function Prowlarr({ service }) { const indexers = indexersData?.filter((indexer) => indexer.enable === true); - let numberOfGrabs = 0 - let numberOfQueries = 0 - let numberOfFailedGrabs = 0 - let numberOfFailedQueries = 0 - grabsData?.indexers?.forEach(element => { + 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; diff --git a/src/components/services/widgets/service/qbittorrent.jsx b/src/components/services/widgets/service/qbittorrent.jsx index 954b9b05b..e7030cd88 100644 --- a/src/components/services/widgets/service/qbittorrent.jsx +++ b/src/components/services/widgets/service/qbittorrent.jsx @@ -4,14 +4,14 @@ import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; -import { formatApiUrl } from "utils/api-helpers"; +import { formatProxyUrl } from "utils/api-helpers"; -export default function QBittorrent ({ service }) { +export default function QBittorrent({ service }) { const { t } = useTranslation(); const config = service.widget; - const { data: torrentData, error: torrentError } = useSWR(formatApiUrl(config, "torrents/info")); + const { data: torrentData, error: torrentError } = useSWR(formatProxyUrl(config, "torrents/info")); if (torrentError) { return ; diff --git a/src/components/services/widgets/service/radarr.jsx b/src/components/services/widgets/service/radarr.jsx index 5fc44ff02..f738ab71b 100644 --- a/src/components/services/widgets/service/radarr.jsx +++ b/src/components/services/widgets/service/radarr.jsx @@ -4,14 +4,15 @@ import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; +import { formatProxyUrl } from "utils/api-helpers"; export default function Radarr({ service }) { const { t } = useTranslation(); const config = service.widget; - const { data: moviesData, error: moviesError } = useSWR(formatApiUrl(config, "movie")); - const { data: queuedData, error: queuedError } = useSWR(formatApiUrl(config, "queue/status")); + const { data: moviesData, error: moviesError } = useSWR(formatProxyUrl(config, "movie")); + const { data: queuedData, error: queuedError } = useSWR(formatProxyUrl(config, "queue/status")); if (moviesError || queuedError) { return ; diff --git a/src/components/services/widgets/service/readarr.jsx b/src/components/services/widgets/service/readarr.jsx index 2f4787683..aab6290a4 100644 --- a/src/components/services/widgets/service/readarr.jsx +++ b/src/components/services/widgets/service/readarr.jsx @@ -4,16 +4,16 @@ import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; -import { formatApiUrl } from "utils/api-helpers"; +import { formatProxyUrl } from "utils/api-helpers"; export default function Readarr({ service }) { const { t } = useTranslation(); const config = service.widget; - const { data: booksData, error: booksError } = useSWR(formatApiUrl(config, "book")); - const { data: wantedData, error: wantedError } = useSWR(formatApiUrl(config, "wanted/missing")); - const { data: queueData, error: queueError } = useSWR(formatApiUrl(config, "queue/status")); + 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 ; diff --git a/src/components/services/widgets/service/rutorrent.jsx b/src/components/services/widgets/service/rutorrent.jsx index ecbe24b5c..6aba5e67f 100644 --- a/src/components/services/widgets/service/rutorrent.jsx +++ b/src/components/services/widgets/service/rutorrent.jsx @@ -4,14 +4,14 @@ import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; -import { formatApiUrl } from "utils/api-helpers"; +import { formatProxyUrl } from "utils/api-helpers"; export default function Rutorrent({ service }) { const { t } = useTranslation(); const config = service.widget; - const { data: statusData, error: statusError } = useSWR(formatApiUrl(config)); + const { data: statusData, error: statusError } = useSWR(formatProxyUrl(config)); if (statusError) { return ; diff --git a/src/components/services/widgets/service/sabnzbd.jsx b/src/components/services/widgets/service/sabnzbd.jsx index 14caa4dc4..a79e11feb 100644 --- a/src/components/services/widgets/service/sabnzbd.jsx +++ b/src/components/services/widgets/service/sabnzbd.jsx @@ -4,14 +4,14 @@ import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; -import { formatApiUrl } from "utils/api-helpers"; +import { formatProxyUrl } from "utils/api-helpers"; export default function SABnzbd({ service }) { const { t } = useTranslation(); const config = service.widget; - const { data: queueData, error: queueError } = useSWR(formatApiUrl(config, "queue")); + const { data: queueData, error: queueError } = useSWR(formatProxyUrl(config, "queue")); if (queueError) { return ; diff --git a/src/components/services/widgets/service/sonarr.jsx b/src/components/services/widgets/service/sonarr.jsx index 584a3d78c..ea91388bc 100644 --- a/src/components/services/widgets/service/sonarr.jsx +++ b/src/components/services/widgets/service/sonarr.jsx @@ -4,16 +4,16 @@ import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; -import { formatApiUrl } from "utils/api-helpers"; +import { formatProxyUrl } from "utils/api-helpers"; export default function Sonarr({ service }) { const { t } = useTranslation(); const config = service.widget; - const { data: wantedData, error: wantedError } = useSWR(formatApiUrl(config, "wanted/missing")); - const { data: queuedData, error: queuedError } = useSWR(formatApiUrl(config, "queue")); - const { data: seriesData, error: seriesError } = useSWR(formatApiUrl(config, "series")); + const { data: wantedData, error: wantedError } = useSWR(formatProxyUrl(config, "wanted/missing")); + const { data: queuedData, error: queuedError } = useSWR(formatProxyUrl(config, "queue")); + const { data: seriesData, error: seriesError } = useSWR(formatProxyUrl(config, "series")); if (wantedError || queuedError || seriesError) { return ; diff --git a/src/components/services/widgets/service/speedtest.jsx b/src/components/services/widgets/service/speedtest.jsx index a4416b837..def1336ec 100644 --- a/src/components/services/widgets/service/speedtest.jsx +++ b/src/components/services/widgets/service/speedtest.jsx @@ -4,14 +4,14 @@ import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; -import { formatApiUrl } from "utils/api-helpers"; +import { formatProxyUrl } from "utils/api-helpers"; export default function Speedtest({ service }) { const { t } = useTranslation(); const config = service.widget; - const { data: speedtestData, error: speedtestError } = useSWR(formatApiUrl(config, "speedtest/latest")); + const { data: speedtestData, error: speedtestError } = useSWR(formatProxyUrl(config, "speedtest/latest")); if (speedtestError) { return ; diff --git a/src/components/services/widgets/service/strelaysrv.jsx b/src/components/services/widgets/service/strelaysrv.jsx index 5533d89a6..a1aff9943 100644 --- a/src/components/services/widgets/service/strelaysrv.jsx +++ b/src/components/services/widgets/service/strelaysrv.jsx @@ -4,14 +4,14 @@ import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; -import { formatApiUrl } from "utils/api-helpers"; +import { formatProxyUrl } from "utils/api-helpers"; export default function StRelaySrv({ service }) { const { t } = useTranslation(); const config = service.widget; - const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, `status`)); + const { data: statsData, error: statsError } = useSWR(formatProxyUrl(config, `status`)); if (statsError) { return ; @@ -29,10 +29,16 @@ export default function StRelaySrv({ service }) { return ( - - - - + + + + ); } diff --git a/src/components/services/widgets/service/tautulli.jsx b/src/components/services/widgets/service/tautulli.jsx index 54fc654b8..9ba4306f8 100644 --- a/src/components/services/widgets/service/tautulli.jsx +++ b/src/components/services/widgets/service/tautulli.jsx @@ -6,7 +6,7 @@ import { MdOutlineSmartDisplay, MdSmartDisplay } from "react-icons/md"; import Widget from "../widget"; -import { formatApiUrl } from "utils/api-helpers"; +import { formatProxyUrl } from "utils/api-helpers"; function millisecondsToTime(milliseconds) { const seconds = Math.floor((milliseconds / 1000) % 60); @@ -120,7 +120,7 @@ export default function Tautulli({ service }) { const config = service.widget; - const { data: activityData, error: activityError } = useSWR(formatApiUrl(config, "get_activity"), { + const { data: activityData, error: activityError } = useSWR(formatProxyUrl(config, "get_activity"), { refreshInterval: 5000, }); diff --git a/src/components/services/widgets/service/traefik.jsx b/src/components/services/widgets/service/traefik.jsx index 816a40a6c..efef287e7 100644 --- a/src/components/services/widgets/service/traefik.jsx +++ b/src/components/services/widgets/service/traefik.jsx @@ -4,14 +4,14 @@ import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; -import { formatApiUrl } from "utils/api-helpers"; +import { formatProxyUrl } from "utils/api-helpers"; export default function Traefik({ service }) { const { t } = useTranslation(); const config = service.widget; - const { data: traefikData, error: traefikError } = useSWR(formatApiUrl(config, "overview")); + const { data: traefikData, error: traefikError } = useSWR(formatProxyUrl(config, "overview")); if (traefikError) { return ; diff --git a/src/components/services/widgets/service/transmission.jsx b/src/components/services/widgets/service/transmission.jsx index bb3eeea69..fb449e281 100644 --- a/src/components/services/widgets/service/transmission.jsx +++ b/src/components/services/widgets/service/transmission.jsx @@ -4,14 +4,14 @@ import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; -import { formatApiUrl } from "utils/api-helpers"; +import { formatProxyUrl } from "utils/api-helpers"; export default function Transmission({ service }) { const { t } = useTranslation(); const config = service.widget; - const { data: torrentData, error: torrentError } = useSWR(formatApiUrl(config)); + const { data: torrentData, error: torrentError } = useSWR(formatProxyUrl(config)); if (torrentError) { return ; @@ -37,7 +37,7 @@ export default function Transmission({ service }) { const torrent = torrents[i]; rateDl += torrent.rateDownload; rateUl += torrent.rateUpload; - if (torrent.percentDone === 1) { + if (torrent.percentDone === 1) { completed += 1; } } diff --git a/src/pages/api/services/proxy.js b/src/pages/api/services/proxy.js index 5b1cbd938..b73ec40bc 100644 --- a/src/pages/api/services/proxy.js +++ b/src/pages/api/services/proxy.js @@ -1,128 +1,45 @@ import createLogger from "utils/logger"; import genericProxyHandler from "utils/proxies/generic"; -import credentialedProxyHandler from "utils/proxies/credentialed"; -import rutorrentProxyHandler from "utils/proxies/rutorrent"; -import nzbgetProxyHandler from "utils/proxies/nzbget"; -import npmProxyHandler from "utils/proxies/npm"; -import transmissionProxyHandler from "utils/proxies/transmission"; -import qbittorrentProxyHandler from "utils/proxies/qbittorrent"; +import widgets from "widgets/widgets"; -const logger = createLogger('servicesProxy'); - -function asJson(data) { - if (data?.length > 0) { - const json = JSON.parse(data.toString()); - return json; - } - return data; -} - -function jsonArrayTransform(data, transform) { - const json = asJson(data); - if (json instanceof Array) { - return transform(json); - } - return json; -} - -function jsonArrayFilter(data, filter) { - return jsonArrayTransform(data, (items) => items.filter(filter)); -} - -const serviceProxyHandlers = { - // uses query param auth - emby: genericProxyHandler, - jellyfin: genericProxyHandler, - pihole: genericProxyHandler, - radarr: { - proxy: genericProxyHandler, - maps: { - movie: (data) => ({ - wanted: jsonArrayFilter(data, (item) => item.isAvailable === false).length, - have: jsonArrayFilter(data, (item) => item.isAvailable === true).length, - }), - }, - }, - sonarr: { - proxy: genericProxyHandler, - maps: { - series: (data) => ({ - total: asJson(data).length, - }), - }, - }, - lidarr: { - proxy: genericProxyHandler, - maps: { - album: (data) => ({ - have: jsonArrayFilter(data, (item) => item?.statistics?.percentOfTracks === 100).length, - }), - }, - }, - readarr: { - proxy: genericProxyHandler, - maps: { - book: (data) => ({ - have: jsonArrayFilter(data, (item) => item?.statistics?.bookFileCount > 0).length, - }), - }, - }, - bazarr: { - proxy: genericProxyHandler, - maps: { - movies: (data) => ({ - total: asJson(data).total, - }), - episodes: (data) => ({ - total: asJson(data).total, - }), - }, - }, - speedtest: genericProxyHandler, - tautulli: genericProxyHandler, - traefik: genericProxyHandler, - sabnzbd: genericProxyHandler, - jackett: genericProxyHandler, - adguard: genericProxyHandler, - strelaysrv: genericProxyHandler, - mastodon: genericProxyHandler, - // uses X-API-Key (or similar) header auth - gotify: credentialedProxyHandler, - portainer: credentialedProxyHandler, - jellyseerr: credentialedProxyHandler, - overseerr: credentialedProxyHandler, - ombi: credentialedProxyHandler, - coinmarketcap: credentialedProxyHandler, - prowlarr: credentialedProxyHandler, - // super specific handlers - rutorrent: rutorrentProxyHandler, - nzbget: nzbgetProxyHandler, - npm: npmProxyHandler, - transmission: transmissionProxyHandler, - qbittorrent: qbittorrentProxyHandler, -}; +const logger = createLogger("servicesProxy"); export default async function handler(req, res) { try { const { type } = req.query; + const widget = widgets[type]; + + if (!widget) { + logger.debug("Unknown proxy service type: %s", type); + return res.status(403).json({ error: "Unkown proxy service type" }); + } + + const serviceProxyHandler = widget.proxyHandler || genericProxyHandler; - const serviceProxyHandler = serviceProxyHandlers[type]; + if (serviceProxyHandler instanceof Function) { + // map opaque endpoints to their actual endpoint + const mapping = widget?.mappings?.[req.query.endpoint]; + const map = mapping?.map; + const endpoint = mapping?.endpoint; + const endpointProxy = mapping?.proxyHandler; - if (serviceProxyHandler) { - if (serviceProxyHandler instanceof Function) { - return serviceProxyHandler(req, res); + if (!endpoint) { + logger.debug("Unsupported service endpoint: %s", type); + return res.status(403).json({ error: "Unsupported service endpoint" }); } - - const { proxy, maps } = serviceProxyHandler; - if (proxy) { - return proxy(req, res, maps); + + req.query.endpoint = endpoint; + + if (endpointProxy instanceof Function) { + return endpointProxy(req, res, map); } + + return serviceProxyHandler(req, res, map); } logger.debug("Unknown proxy service type: %s", type); return res.status(403).json({ error: "Unkown proxy service type" }); - } - catch (ex) { + } catch (ex) { logger.error(ex); return res.status(500).send({ error: "Unexpected error" }); } diff --git a/src/utils/api-helpers.js b/src/utils/api-helpers.js index 4b5b72d18..c0a2314a4 100644 --- a/src/utils/api-helpers.js +++ b/src/utils/api-helpers.js @@ -1,44 +1,44 @@ -const formats = { - emby: `{url}/emby/{endpoint}?api_key={key}`, - jellyfin: `{url}/emby/{endpoint}?api_key={key}`, - pihole: `{url}/admin/{endpoint}`, - radarr: `{url}/api/v3/{endpoint}?apikey={key}`, - sonarr: `{url}/api/v3/{endpoint}?apikey={key}`, - speedtest: `{url}/api/{endpoint}`, - tautulli: `{url}/api/v2?apikey={key}&cmd={endpoint}`, - traefik: `{url}/api/{endpoint}`, - portainer: `{url}/api/endpoints/{env}/{endpoint}`, - rutorrent: `{url}/plugins/httprpc/action.php`, - transmission: `{url}/transmission/rpc`, - qbittorrent: `{url}/api/v2/{endpoint}`, - jellyseerr: `{url}/api/v1/{endpoint}`, - overseerr: `{url}/api/v1/{endpoint}`, - ombi: `{url}/api/v1/{endpoint}`, - npm: `{url}/api/{endpoint}`, - lidarr: `{url}/api/v1/{endpoint}?apikey={key}`, - readarr: `{url}/api/v1/{endpoint}?apikey={key}`, - bazarr: `{url}/api/{endpoint}/wanted?apikey={key}`, - sabnzbd: `{url}/api/?apikey={key}&output=json&mode={endpoint}`, - coinmarketcap: `https://pro-api.coinmarketcap.com/{endpoint}`, - gotify: `{url}/{endpoint}`, - prowlarr: `{url}/api/v1/{endpoint}`, - jackett: `{url}/api/v2.0/{endpoint}?apikey={key}&configured=true`, - adguard: `{url}/control/{endpoint}`, - strelaysrv: `{url}/{endpoint}`, - mastodon: `{url}/api/v1/{endpoint}`, -}; +// const formats = { +// emby: `{url}/emby/{endpoint}?api_key={key}`, +// jellyfin: `{url}/emby/{endpoint}?api_key={key}`, +// pihole: `{url}/admin/{endpoint}`, +// radarr: `{url}/api/v3/{endpoint}?apikey={key}`, +// sonarr: `{url}/api/v3/{endpoint}?apikey={key}`, +// speedtest: `{url}/api/{endpoint}`, +// tautulli: `{url}/api/v2?apikey={key}&cmd={endpoint}`, +// traefik: `{url}/api/{endpoint}`, +// portainer: `{url}/api/endpoints/{env}/{endpoint}`, +// rutorrent: `{url}/plugins/httprpc/action.php`, +// transmission: `{url}/transmission/rpc`, +// qbittorrent: `{url}/api/v2/{endpoint}`, +// jellyseerr: `{url}/api/v1/{endpoint}`, +// overseerr: `{url}/api/v1/{endpoint}`, +// ombi: `{url}/api/v1/{endpoint}`, +// npm: `{url}/api/{endpoint}`, +// lidarr: `{url}/api/v1/{endpoint}?apikey={key}`, +// readarr: `{url}/api/v1/{endpoint}?apikey={key}`, +// bazarr: `{url}/api/{endpoint}/wanted?apikey={key}`, +// sabnzbd: `{url}/api/?apikey={key}&output=json&mode={endpoint}`, +// coinmarketcap: `https://pro-api.coinmarketcap.com/{endpoint}`, +// gotify: `{url}/{endpoint}`, +// prowlarr: `{url}/api/v1/{endpoint}`, +// jackett: `{url}/api/v2.0/{endpoint}?apikey={key}&configured=true`, +// adguard: `{url}/control/{endpoint}`, +// strelaysrv: `{url}/{endpoint}`, +// mastodon: `{url}/api/v1/{endpoint}`, +// }; -export function formatApiCall(api, args) { +export function formatApiCall(url, args) { const find = /\{.*?\}/g; const replace = (match) => { const key = match.replace(/\{|\}/g, ""); return args[key]; }; - return formats[api].replace(find, replace); + return url.replace(find, replace); } -export function formatApiUrl(widget, endpoint) { +export function formatProxyUrl(widget, endpoint) { const params = new URLSearchParams({ type: widget.type, group: widget.service_group, @@ -47,3 +47,23 @@ export function formatApiUrl(widget, endpoint) { }); return `/api/services/proxy?${params.toString()}`; } + +export function asJson(data) { + if (data?.length > 0) { + const json = JSON.parse(data.toString()); + return json; + } + return data; +} + +export function jsonArrayTransform(data, transform) { + const json = asJson(data); + if (json instanceof Array) { + return transform(json); + } + return json; +} + +export function jsonArrayFilter(data, filter) { + return jsonArrayTransform(data, (items) => items.filter(filter)); +} diff --git a/src/utils/proxies/credentialed.js b/src/utils/proxies/credentialed.js index 736da48e0..016770a6d 100644 --- a/src/utils/proxies/credentialed.js +++ b/src/utils/proxies/credentialed.js @@ -1,6 +1,7 @@ import getServiceWidget from "utils/service-helpers"; import { formatApiCall } from "utils/api-helpers"; import { httpProxy } from "utils/http"; +import widgets from "widgets/widgets"; export default async function credentialedProxyHandler(req, res) { const { group, service, endpoint } = req.query; @@ -8,8 +9,12 @@ export default async function credentialedProxyHandler(req, res) { if (group && service) { const widget = await getServiceWidget(group, service); + if (!widgets?.[widget.type]?.api) { + return res.status(403).json({ error: "Service does not support API calls" }); + } + if (widget) { - const url = new URL(formatApiCall(widget.type, { endpoint, ...widget })); + const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget })); const headers = { "Content-Type": "application/json", diff --git a/src/utils/proxies/generic.js b/src/utils/proxies/generic.js index 70dab4aa1..0f911aeb6 100644 --- a/src/utils/proxies/generic.js +++ b/src/utils/proxies/generic.js @@ -2,17 +2,22 @@ import getServiceWidget from "utils/service-helpers"; import { formatApiCall } from "utils/api-helpers"; import { httpProxy } from "utils/http"; import createLogger from "utils/logger"; +import widgets from "widgets/widgets"; -const logger = createLogger('genericProxyHandler'); +const logger = createLogger("genericProxyHandler"); -export default async function genericProxyHandler(req, res, maps) { +export default async function genericProxyHandler(req, res, map) { const { group, service, endpoint } = req.query; if (group && service) { const widget = await getServiceWidget(group, service); + if (!widgets?.[widget.type]?.api) { + return res.status(403).json({ error: "Service does not support API calls" }); + } + if (widget) { - const url = new URL(formatApiCall(widget.type, { endpoint, ...widget })); + const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget })); let headers; if (widget.username && widget.password) { @@ -27,8 +32,8 @@ export default async function genericProxyHandler(req, res, maps) { }); let resultData = data; - if ((status === 200) && (maps?.[endpoint])) { - resultData = maps[endpoint](data); + if (status === 200 && map) { + resultData = map(data); } if (contentType) res.setHeader("Content-Type", contentType); diff --git a/src/utils/proxies/npm.js b/src/utils/proxies/npm.js index d60cedd1a..bd612a504 100644 --- a/src/utils/proxies/npm.js +++ b/src/utils/proxies/npm.js @@ -1,5 +1,6 @@ import getServiceWidget from "utils/service-helpers"; import { formatApiCall } from "utils/api-helpers"; +import widgets from "widgets/widgets"; export default async function npmProxyHandler(req, res) { const { group, service, endpoint } = req.query; @@ -7,8 +8,12 @@ export default async function npmProxyHandler(req, res) { if (group && service) { const widget = await getServiceWidget(group, service); + if (!widgets?.[widget.type]?.api) { + return res.status(403).json({ error: "Service does not support API calls" }); + } + if (widget) { - const url = new URL(formatApiCall(widget.type, { endpoint, ...widget })); + const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget })); const loginUrl = `${widget.url}/api/tokens`; const body = { identity: widget.username, secret: widget.password }; diff --git a/src/utils/proxies/qbittorrent.js b/src/utils/proxies/qbittorrent.js index df3410002..dfb46a258 100644 --- a/src/utils/proxies/qbittorrent.js +++ b/src/utils/proxies/qbittorrent.js @@ -12,15 +12,15 @@ async function login(widget, params) { return fetch(loginUrl, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, - body: loginBody + body: loginBody, }) - .then(async response => { - addCookieToJar(loginUrl, response.headers); - setCookieHeader(loginUrl, params); - const data = await response.text(); - return ([response.status, data]); - }) - .catch(err => ([500, err])); + .then(async (response) => { + addCookieToJar(loginUrl, response.headers); + setCookieHeader(loginUrl, params); + const data = await response.text(); + return [response.status, data]; + }) + .catch((err) => [500, err]); } export default async function qbittorrentProxyHandler(req, res) { @@ -46,7 +46,7 @@ export default async function qbittorrentProxyHandler(req, res) { if (status !== 200) { return res.status(status).end(data); } - if (data.toString() !== 'Ok.') { + if (data.toString() !== "Ok.") { return res.status(401).end(data); } } @@ -55,4 +55,4 @@ export default async function qbittorrentProxyHandler(req, res) { if (contentType) res.setHeader("Content-Type", contentType); return res.status(status).send(data); -} \ No newline at end of file +} diff --git a/src/widgets/components.js b/src/widgets/components.js new file mode 100644 index 000000000..086da75a2 --- /dev/null +++ b/src/widgets/components.js @@ -0,0 +1,8 @@ +import dynamic from "next/dynamic"; + +const components = { + overseerr: dynamic(() => import("./overseerr/component")), + radarr: dynamic(() => import("./radarr/component")), +}; + +export default components; diff --git a/src/widgets/overseerr/component.jsx b/src/widgets/overseerr/component.jsx new file mode 100644 index 000000000..0201ca81c --- /dev/null +++ b/src/widgets/overseerr/component.jsx @@ -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: statsData, error: statsError } = useSWR(formatProxyUrl(config, "request/count")); + + if (statsError) { + return ; + } + + if (!statsData) { + return ( + + + + + + ); + } + + return ( + + + + + + ); +} diff --git a/src/widgets/overseerr/widget.js b/src/widgets/overseerr/widget.js new file mode 100644 index 000000000..b46bad902 --- /dev/null +++ b/src/widgets/overseerr/widget.js @@ -0,0 +1,14 @@ +import credentialedProxyHandler from "utils/proxies/credentialed"; + +const widget = { + api: "{url}/api/v1/{endpoint}", + proxyHandler: credentialedProxyHandler, + + mappings: { + "request/count": { + endpoint: "request/count", + }, + }, +}; + +export default widget; diff --git a/src/widgets/radarr/component.jsx b/src/widgets/radarr/component.jsx new file mode 100644 index 000000000..94b85acd3 --- /dev/null +++ b/src/widgets/radarr/component.jsx @@ -0,0 +1,37 @@ +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: moviesData, error: moviesError } = useSWR(formatProxyUrl(config, "movie")); + const { data: queuedData, error: queuedError } = useSWR(formatProxyUrl(config, "queue/status")); + + if (moviesError || queuedError) { + return ; + } + + if (!moviesData || !queuedData) { + return ( + + + + + + ); + } + + return ( + + + + + + ); +} diff --git a/src/widgets/radarr/widget.js b/src/widgets/radarr/widget.js new file mode 100644 index 000000000..8e8325592 --- /dev/null +++ b/src/widgets/radarr/widget.js @@ -0,0 +1,22 @@ +import genericProxyHandler from "utils/proxies/generic"; +import { jsonArrayFilter } from "utils/api-helpers"; + +const widget = { + api: "{url}/api/v3/{endpoint}?apikey={key}", + proxyHandler: genericProxyHandler, + + mappings: { + movie: { + endpoint: "movie", + map: (data) => ({ + wanted: jsonArrayFilter(data, (item) => item.isAvailable === false).length, + have: jsonArrayFilter(data, (item) => item.isAvailable === true).length, + }), + }, + "queue/status": { + endpoint: "queue/status", + }, + }, +}; + +export default widget; diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js new file mode 100644 index 000000000..db4f22efc --- /dev/null +++ b/src/widgets/widgets.js @@ -0,0 +1,9 @@ +import overseerr from "./overseerr/widget"; +import radarr from "./radarr/widget"; + +const widgets = { + overseerr, + radarr, +}; + +export default widgets; diff --git a/tailwind.config.js b/tailwind.config.js index d9b2ea0ef..b6e27a307 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -4,7 +4,11 @@ const tailwindScrollbars = require("tailwind-scrollbar"); module.exports = { darkMode: "class", - content: ["./src/pages/**/*.{js,ts,jsx,tsx}", "./src/components/**/*.{js,ts,jsx,tsx}"], + content: [ + "./src/pages/**/*.{js,ts,jsx,tsx}", + "./src/components/**/*.{js,ts,jsx,tsx}", + "./src/widgets/**/*.{js,ts,jsx,tsx}", + ], theme: { extend: { colors: {