pull/121/head
Francisco Coelho 2 years ago
commit 0c9c1c599f

@ -19,6 +19,7 @@
* Service Integration
- Currently supports Sonarr, Radarr, Ombi, Emby, Jellyfin, Tautulli (Plex), Overseerr, Jellyseerr ([ilusi0n](https://github.com/benphelps/homepage/pull/34)), NZBGet, ruTorrent
- Portainer, Traefik, Speedtest Tracker, PiHole, Nginx Proxy Manager ([aidenpwnz](https://github.com/benphelps/homepage/pull/45))
- Readarr, SABnzbd ([JazzFisch]())
* Information & Utility Widgets
- System Stats (Disk, CPU, Memory)
- Weather via WeatherAPI.com or OpenWeatherMap ([AlexFullmoon](https://github.com/benphelps/homepage/pull/25))

@ -12,6 +12,7 @@
"@headlessui/react": "^1.6.6",
"@tailwindcss/forms": "^0.5.3",
"classnames": "^2.3.1",
"currency-symbol-map": "^5.1.0",
"dockerode": "^3.3.4",
"i18next": "^21.9.1",
"i18next-browser-languagedetector": "^6.1.5",

@ -5,6 +5,7 @@ specifiers:
'@tailwindcss/forms': ^0.5.3
autoprefixer: ^10.4.8
classnames: ^2.3.1
currency-symbol-map: ^5.1.0
dockerode: ^3.3.4
eslint: 8.22.0
eslint-config-airbnb: ^19.0.4
@ -41,6 +42,7 @@ dependencies:
'@headlessui/react': 1.6.6_biqbaboplfbrettd7655fr4n2y
'@tailwindcss/forms': 0.5.3_tailwindcss@3.1.8
classnames: 2.3.1
currency-symbol-map: 5.1.0
dockerode: 3.3.4
i18next: 21.9.1
i18next-browser-languagedetector: 6.1.5
@ -699,6 +701,10 @@ packages:
engines: {node: '>=4'}
hasBin: true
/currency-symbol-map/5.1.0:
resolution: {integrity: sha512-LO/lzYRw134LMDVnLyAf1dHE5tyO6axEFkR3TXjQIOmMkAM9YL6QsiUwuXzZAmFnuDJcs4hayOgyIYtViXFrLw==}
dev: false
/damerau-levenshtein/1.0.8:
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
dev: true

@ -31,11 +31,6 @@
"bitrate": "Bitrate",
"no_active": "No Active Streams"
},
"nzbget": {
"rate": "Rate",
"remaining": "Verblieben",
"downloaded": "Heruntergeladen"
},
"rutorrent": {
"active": "Aktiv",
"upload": "Hochladen",
@ -51,6 +46,11 @@
"queued": "In Warteschlange",
"movies": "Filme"
},
"readarr": {
"wanted": "Wanted",
"queued": "Queued",
"books": "Books"
},
"ombi": {
"pending": "Ausstehend",
"approved": "Genehmigt",
@ -97,15 +97,22 @@
"approved": "Approved",
"available": "Available"
},
"gotify": {
"apps": "Anwendungen",
"clients": "Kunden",
"messages": "Mitteilungen"
},
"sabnzbd": {
"status": "Status",
"speed": "Speed",
"rate": "Rate",
"queue": "Queue",
"timeleft": "Time Left"
},
"nzbget": {
"rate": "Rate",
"remaining": "Remaining",
"timeleft": "Time left"
}
"downloaded": "Downloaded"
},
"coinmarketcap": {
"configure": "Configure one or more crypto currencies to track"
},
"gotify": {
"apps": "Applications",
"clients": "Clients",
"messages": "Messages"
},
}

@ -53,6 +53,11 @@
"remaining": "Remaining",
"downloaded": "Downloaded"
},
"sabnzbd": {
"rate": "Rate",
"queue": "Queue",
"timeleft": "Time Left"
},
"rutorrent": {
"active": "Active",
"upload": "Upload",
@ -68,6 +73,11 @@
"queued": "Queued",
"movies": "Movies"
},
"readarr": {
"wanted": "Wanted",
"queued": "Queued",
"books": "Books"
},
"ombi": {
"pending": "Pending",
"approved": "Approved",
@ -108,15 +118,12 @@
"disabled": "Disabled",
"total": "Total"
},
"coinmarketcap": {
"configure": "Configure one or more crypto currencies to track"
},
"gotify": {
"apps": "Applications",
"clients": "Clients",
"messages": "Messages"
},
"sabnzbd": {
"status": "Status",
"speed": "Speed",
"remaining": "Remaining",
"timeleft": "Time left"
}
}

@ -23,18 +23,13 @@
"playing": "En ejecución",
"transcoding": "Transcodificando",
"bitrate": "Tasa de Bits",
"no_active": "No Active Streams"
"no_active": "No hay streams activos"
},
"tautulli": {
"playing": "En ejecución",
"transcoding": "Transcodificación",
"bitrate": "Tasa de bits",
"no_active": "No Active Streams"
},
"nzbget": {
"rate": "Velocidad",
"remaining": "Restante",
"downloaded": "Descargado"
"no_active": "No hay streams activos"
},
"rutorrent": {
"active": "Activo",
@ -51,6 +46,11 @@
"queued": "Puesto en cola",
"movies": "Películas"
},
"readarr": {
"wanted": "Wanted",
"queued": "Queued",
"books": "Books"
},
"ombi": {
"pending": "Pendiente",
"approved": "Aprobado",
@ -93,19 +93,26 @@
"wait": "Espere, por favor"
},
"overseerr": {
"pending": "Pending",
"approved": "Approved",
"available": "Available"
},
"gotify": {
"apps": "Aplicaciones",
"clients": "Clientela",
"messages": "Mensajes"
"pending": "Pendiente",
"approved": "Aprobado",
"available": "Disponible"
},
"sabnzbd": {
"status": "Status",
"speed": "Speed",
"rate": "Rate",
"queue": "Queue",
"timeleft": "Time Left"
},
"nzbget": {
"rate": "Rate",
"remaining": "Remaining",
"timeleft": "Time left"
}
"downloaded": "Downloaded"
},
"coinmarketcap": {
"configure": "Configure one or more crypto currencies to track"
},
"gotify": {
"apps": "Applications",
"clients": "Clients",
"messages": "Messages"
},
}

@ -8,33 +8,28 @@
"placeholder": "Recherche…"
},
"resources": {
"total": "Totale",
"total": "Total",
"free": "Libre",
"used": "Utilisée"
},
"docker": {
"rx": "Rx",
"tx": "TX",
"mem": "Mem",
"cpu": "CPU",
"tx": "Tx",
"mem": "Mém",
"cpu": "Cpu",
"offline": "Hors ligne"
},
"emby": {
"playing": "En jouant",
"transcoding": "Transcoding",
"bitrate": "Débiter",
"no_active": "No Active Streams"
"playing": "En lecture",
"transcoding": "Transcodage",
"bitrate": "Débit",
"no_active": "Aucun flux actif"
},
"tautulli": {
"playing": "En jouant",
"transcoding": "Transcoding",
"bitrate": "Débiter",
"no_active": "No Active Streams"
},
"nzbget": {
"rate": "Évaluer",
"remaining": "Restante",
"downloaded": "Téléchargé"
"playing": "En lecture",
"transcoding": "Transcodage",
"bitrate": "Débit",
"no_active": "Aucun flux actif"
},
"rutorrent": {
"active": "Active",
@ -44,13 +39,18 @@
"sonarr": {
"wanted": "Recherchée",
"queued": "En queue",
"series": "Série"
"series": "Séries"
},
"radarr": {
"wanted": "Recherchée",
"queued": "En queue",
"movies": "Films"
},
"readarr": {
"wanted": "Wanted",
"queued": "Queued",
"books": "Books"
},
"ombi": {
"pending": "En attente",
"approved": "Approuvée",
@ -74,7 +74,7 @@
"portainer": {
"running": "Fonctionnement",
"stopped": "Arrêté",
"total": "Totale"
"total": "Total"
},
"traefik": {
"routers": "Routeurs",
@ -83,8 +83,8 @@
},
"npm": {
"enabled": "Activé",
"disabled": "Handicapée",
"total": "Totale"
"disabled": "Désactivé",
"total": "Total"
},
"common": {
"bbytes": "{{value, bytes(binary: true)}}",
@ -104,19 +104,26 @@
"wait": "Veuillez patienter"
},
"overseerr": {
"pending": "Pending",
"approved": "Approved",
"available": "Available"
"pending": "En attente",
"approved": "Approuvée",
"available": "Disponible"
},
"sabnzbd": {
"rate": "Rate",
"queue": "Queue",
"timeleft": "Time Left"
},
"nzbget": {
"remaining": "Remaining",
"downloaded": "Downloaded",
"rate": "Rate"
},
"coinmarketcap": {
"configure": "Configure one or more crypto currencies to track"
},
"gotify": {
"apps": "Applications",
"clients": "Clients",
"messages": "Messages"
},
"sabnzbd": {
"status": "Status",
"speed": "Speed",
"remaining": "Remaining",
"timeleft": "Time left"
}
}

@ -7,16 +7,16 @@
"rx": "RX"
},
"emby": {
"playing": "Playing",
"playing": "In riproduzione",
"transcoding": "Transcoding",
"bitrate": "Bitrate",
"no_active": "No Active Streams"
"no_active": "Nessuno Stream Attivo"
},
"tautulli": {
"playing": "Playing",
"playing": "In riproduzione",
"transcoding": "Transcoding",
"bitrate": "Bitrate",
"no_active": "No Active Streams"
"no_active": "Nessuno Stream Attivo"
},
"speedtest": {
"upload": "Upload",
@ -24,88 +24,95 @@
"ping": "Ping"
},
"portainer": {
"running": "Running",
"stopped": "Stopped",
"total": "Total"
"running": "In esecuzione",
"stopped": "Fermati",
"total": "Totali"
},
"traefik": {
"routers": "Routers",
"services": "Services",
"services": "Servizi",
"middleware": "Middleware"
},
"widget": {
"missing_type": "Missing Widget Type: {{type}}",
"api_error": "API Error",
"status": "Status"
"api_error": "Errore API",
"status": "Stato"
},
"search": {
"placeholder": "Search…"
"placeholder": "Cerca…"
},
"resources": {
"total": "Total",
"free": "Free",
"used": "Used"
},
"nzbget": {
"rate": "Rate",
"remaining": "Remaining",
"downloaded": "Downloaded"
"total": "Totale",
"free": "Libero",
"used": "In utilizzo"
},
"rutorrent": {
"active": "Active",
"active": "Attivo",
"upload": "Upload",
"download": "Download"
},
"sonarr": {
"series": "Series",
"wanted": "Wanted",
"queued": "Queued"
"series": "Serie",
"wanted": "Rchiesti",
"queued": "In coda"
},
"radarr": {
"wanted": "Richiesti",
"queued": "In coda",
"movies": "Film"
},
"readarr": {
"wanted": "Wanted",
"queued": "Queued",
"movies": "Movies"
"books": "Books"
},
"ombi": {
"pending": "Pending",
"approved": "Approved",
"available": "Available"
"pending": "In attesa",
"approved": "Approvati",
"available": "Disponibili"
},
"jellyseerr": {
"pending": "Pending",
"approved": "Approved",
"available": "Available"
"pending": "In attesa",
"approved": "Approvati",
"available": "Disponibili"
},
"pihole": {
"queries": "Queries",
"blocked": "Blocked",
"gravity": "Gravity"
"queries": "Richieste",
"blocked": "Bloccati",
"gravity": "Severità"
},
"npm": {
"enabled": "Enabled",
"disabled": "Disabled",
"total": "Total"
"enabled": "Attivi",
"disabled": "Disabilitati",
"total": "Totali"
},
"weather": {
"current": "Current Location",
"allow": "Click to allow",
"updating": "Updating",
"wait": "Please wait"
"current": "Posizione Attuale",
"allow": "Clicca per consentire",
"updating": "Aggiornamento in corso",
"wait": "Attendi per favore"
},
"overseerr": {
"pending": "Pending",
"approved": "Approved",
"available": "Available"
"pending": "In attesa",
"approved": "Approvati",
"available": "Disponibili"
},
"sabnzbd": {
"rate": "Rate",
"queue": "Queue",
"timeleft": "Time Left"
},
"nzbget": {
"rate": "Rate",
"remaining": "Remaining",
"downloaded": "Downloaded"
},
"coinmarketcap": {
"configure": "Configure one or more crypto currencies to track"
},
"gotify": {
"apps": "Applications",
"clients": "Clients",
"messages": "Messages"
},
"sabnzbd": {
"status": "Status",
"speed": "Speed",
"remaining": "Remaining",
"timeleft": "Time left"
}
}

@ -31,11 +31,6 @@
"bitrate": "Bitrate",
"no_active": "No Active Streams"
},
"nzbget": {
"rate": "Takt",
"remaining": "Gjenstående",
"downloaded": "Nedlastet"
},
"rutorrent": {
"active": "Aktiv",
"upload": "Opplasting",
@ -51,6 +46,11 @@
"queued": "I kø",
"movies": "Filmer"
},
"readarr": {
"wanted": "Wanted",
"queued": "Queued",
"books": "Books"
},
"ombi": {
"pending": "Venter",
"approved": "Godkjent",
@ -97,15 +97,22 @@
"approved": "Approved",
"available": "Available"
},
"sabnzbd": {
"rate": "Rate",
"queue": "Queue",
"timeleft": "Time Left"
},
"nzbget": {
"rate": "Rate",
"downloaded": "Downloaded",
"remaining": "Remaining"
},
"coinmarketcap": {
"configure": "Configure one or more crypto currencies to track"
},
"gotify": {
"apps": "Applications",
"clients": "Clients",
"messages": "Messages"
},
"sabnzbd": {
"status": "Status",
"speed": "Speed",
"remaining": "Remaining",
"timeleft": "Time left"
}
}

@ -16,11 +16,6 @@
"cpu": "CPU",
"offline": "Offline"
},
"nzbget": {
"rate": "Rate",
"remaining": "Overgebleven",
"downloaded": "Gedownload"
},
"speedtest": {
"upload": "Upload",
"download": "Download",
@ -67,6 +62,11 @@
"wanted": "Gezocht",
"queued": "In de wachtrij"
},
"readarr": {
"wanted": "Wanted",
"queued": "Queued",
"books": "Books"
},
"ombi": {
"pending": "In afwachting",
"approved": "Goedgekeurd",
@ -97,15 +97,22 @@
"approved": "Approved",
"available": "Available"
},
"sabnzbd": {
"rate": "Rate",
"queue": "Queue",
"timeleft": "Time Left"
},
"nzbget": {
"rate": "Rate",
"remaining": "Remaining",
"downloaded": "Downloaded"
},
"coinmarketcap": {
"configure": "Configure one or more crypto currencies to track"
},
"gotify": {
"apps": "Applications",
"clients": "Clients",
"messages": "Messages"
},
"sabnzbd": {
"status": "Status",
"speed": "Speed",
"remaining": "Remaining",
"timeleft": "Time left"
}
}

@ -31,11 +31,6 @@
"bitrate": "Taxa de bits",
"no_active": "No Active Streams"
},
"nzbget": {
"rate": "Avaliar",
"remaining": "Em falta",
"downloaded": "Baixada"
},
"rutorrent": {
"active": "Ativa",
"upload": "Envio",
@ -51,6 +46,11 @@
"queued": "Enfileiradas",
"movies": "Filmes"
},
"readarr": {
"wanted": "Wanted",
"queued": "Queued",
"books": "Books"
},
"ombi": {
"pending": "Pendente",
"approved": "Aprovada",
@ -108,15 +108,22 @@
"approved": "Approved",
"available": "Available"
},
"gotify": {
"apps": "Aplicações",
"clients": "Clientes",
"messages": "Mensagens"
},
"sabnzbd": {
"status": "Status",
"speed": "Speed",
"rate": "Rate",
"queue": "Queue",
"timeleft": "Time Left"
},
"nzbget": {
"rate": "Rate",
"remaining": "Remaining",
"timeleft": "Time left"
}
"downloaded": "Downloaded"
},
"coinmarketcap": {
"configure": "Configure one or more crypto currencies to track"
},
"gotify": {
"apps": "Applications",
"clients": "Clients",
"messages": "Messages"
},
}

@ -31,11 +31,6 @@
"bitrate": "Битрейт",
"no_active": "No Active Streams"
},
"nzbget": {
"rate": "Оценка",
"remaining": "Осталось",
"downloaded": "Загружено"
},
"rutorrent": {
"active": "Активный",
"upload": "Загрузить",
@ -51,6 +46,11 @@
"queued": "В очереди",
"movies": "Фильмы"
},
"readarr": {
"wanted": "Wanted",
"queued": "Queued",
"books": "Books"
},
"ombi": {
"pending": "Ожидание",
"approved": "Одобрено",
@ -97,15 +97,22 @@
"approved": "Approved",
"available": "Available"
},
"gotify": {
"apps": "Aplicações",
"clients": "Clientes",
"messages": "Mensagens"
},
"sabnzbd": {
"status": "Status",
"speed": "Speed",
"rate": "Rate",
"queue": "Queue",
"timeleft": "Time Left"
},
"nzbget": {
"rate": "Rate",
"remaining": "Remaining",
"timeleft": "Time left"
}
"downloaded": "Downloaded"
},
"coinmarketcap": {
"configure": "Configure one or more crypto currencies to track"
},
"gotify": {
"apps": "Applications",
"clients": "Clients",
"messages": "Messages"
},
}

@ -31,11 +31,6 @@
"bitrate": "Bitrate",
"no_active": "No Active Streams"
},
"nzbget": {
"rate": "Rate",
"remaining": "Còn lại",
"downloaded": "Đã tải"
},
"rutorrent": {
"active": "Hoạt động",
"upload": "Tải lên",
@ -51,6 +46,11 @@
"queued": "Queued",
"movies": "Movies"
},
"readarr": {
"wanted": "Wanted",
"queued": "Queued",
"books": "Books"
},
"ombi": {
"pending": "Pending",
"approved": "Approved",
@ -97,15 +97,22 @@
"approved": "Approved",
"available": "Available"
},
"gotify": {
"apps": "Aplicações",
"clients": "Clientes",
"messages": "Mensagens"
},
"sabnzbd": {
"status": "Status",
"speed": "Speed",
"rate": "Rate",
"queue": "Queue",
"timeleft": "Time Left"
},
"nzbget": {
"rate": "Rate",
"remaining": "Remaining",
"timeleft": "Time left"
"downloaded": "Downloaded"
},
"coinmarketcap": {
"configure": "Configure one or more crypto currencies to track"
},
"gotify": {
"apps": "Applications",
"clients": "Clients",
"messages": "Messages"
}
}

@ -31,11 +31,6 @@
"bitrate": "比特率",
"no_active": "No Active Streams"
},
"nzbget": {
"rate": "速度",
"remaining": "其余的",
"downloaded": "下载"
},
"rutorrent": {
"active": "积极的",
"upload": "上传",
@ -51,6 +46,11 @@
"queued": "排队",
"movies": "电影"
},
"readarr": {
"wanted": "Wanted",
"queued": "Queued",
"books": "Books"
},
"ombi": {
"pending": "待办的",
"approved": "得到正式认可的",
@ -97,15 +97,22 @@
"approved": "Approved",
"available": "Available"
},
"sabnzbd": {
"rate": "Rate",
"queue": "Queue",
"timeleft": "Time Left"
},
"nzbget": {
"rate": "Rate",
"remaining": "Remaining",
"downloaded": "Downloaded"
},
"coinmarketcap": {
"configure": "Configure one or more crypto currencies to track"
},
"gotify": {
"apps": "Aplicações",
"clients": "Clientes",
"messages": "Mensagens"
},
"sabnzbd": {
"status": "Status",
"speed": "Speed",
"remaining": "Remaining",
"timeleft": "Time left"
}
}

@ -2,10 +2,12 @@ import { useTranslation } from "react-i18next";
import Sonarr from "./widgets/service/sonarr";
import Radarr from "./widgets/service/radarr";
import Readarr from "./widgets/service/readarr";
import Ombi from "./widgets/service/ombi";
import Portainer from "./widgets/service/portainer";
import Emby from "./widgets/service/emby";
import Nzbget from "./widgets/service/nzbget";
import SABnzbd from "./widgets/service/sabnzbd";
import Docker from "./widgets/service/docker";
import Pihole from "./widgets/service/pihole";
import Rutorrent from "./widgets/service/rutorrent";
@ -16,24 +18,27 @@ import Jellyseerr from "./widgets/service/jellyseerr";
import Overseerr from "./widgets/service/overseerr";
import Npm from "./widgets/service/npm";
import Tautulli from "./widgets/service/tautulli";
import CoinMarketCap from "./widgets/service/coinmarketcap";
import Gotify from "./widgets/service/gotify";
import Sabnzbd from "./widgets/service/sabnzbd";
const widgetMappings = {
docker: Docker,
sonarr: Sonarr,
radarr: Radarr,
readarr: Readarr,
ombi: Ombi,
portainer: Portainer,
emby: Emby,
jellyfin: Jellyfin,
nzbget: Nzbget,
sabnzbd: SABnzbd,
pihole: Pihole,
rutorrent: Rutorrent,
speedtest: Speedtest,
traefik: Traefik,
jellyseerr: Jellyseerr,
overseerr: Overseerr,
coinmarketcap: CoinMarketCap,
npm: Npm,
tautulli: Tautulli,
gotify: Gotify,

@ -0,0 +1,64 @@
import useSWR from "swr";
import { useTranslation } from "react-i18next";
import getSymbolFromCurrency from "currency-symbol-map";
import Widget from "../widget";
import Block from "../block";
import { formatApiUrl } from "utils/api-helpers";
export default function CoinMarketCap({ service }) {
const { t } = useTranslation();
const config = service.widget;
const symbols = [...service.symbols];
const currencyCode = service.currency ?? "USD";
const { data: statsData, error: statsError } = useSWR(
formatApiUrl(config, `v1/cryptocurrency/quotes/latest?symbol=${symbols.join(",")}&convert=${currencyCode}`)
);
if (!symbols || symbols.length === 0) {
return (
<Widget>
<Block value={t("coinmarketcap.configure")} />
</Widget>
);
}
if (statsError) {
return <Widget error={t("widget.api_error")} />;
}
if (!statsData) {
return (
<Widget>
<Block value={t("coinmarketcap.configure")} />
</Widget>
);
}
const { data } = statsData;
const currencySymbol = getSymbolFromCurrency(currencyCode);
return symbols.map((key) => (
<Widget key={data[key].symbol}>
<div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-row items-center justify-between p-1">
<div className="font-thin text-sm">{data[key].name}</div>
<div className="flex flex-col text-right">
<div className="font-bold text-xs">
{currencySymbol}
{data[key].quote[currencyCode].price.toFixed(2)}
</div>
<div
className={`font-bold text-xs ${
data[key].quote[currencyCode].percent_change_1h > 0 ? "text-emerald-300" : "text-rose-300"
}`}
>
{data[key].quote[currencyCode].percent_change_1h.toFixed(2)}%
</div>
</div>
</div>
</Widget>
));
}

@ -27,6 +27,7 @@ function ticksToString(ticks) {
}
function SingleSessionEntry({ playCommand, session }) {
console.log(session);
const {
NowPlayingItem: { Name, SeriesName, RunTimeTicks },
PlayState: { PositionTicks, IsPaused, IsMuted },
@ -175,7 +176,7 @@ export default function Emby({ service }) {
if (playing.length === 0) {
return (
<div className="flex flex-col pb-1">
<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("emby.no_active")}</span>
</div>
@ -189,7 +190,7 @@ export default function Emby({ service }) {
if (playing.length === 1) {
const session = playing[0];
return (
<div className="flex flex-col pb-1">
<div className="flex flex-col pb-1 mx-1">
<SingleSessionEntry
playCommand={(currentSession, command) => handlePlayCommand(currentSession, command)}
session={session}
@ -199,7 +200,7 @@ export default function Emby({ service }) {
}
return (
<div className="flex flex-col pb-1">
<div className="flex flex-col pb-1 mx-1">
{playing.map((session) => (
<SessionEntry
key={session.Id}

@ -0,0 +1,41 @@
import useSWR from "swr";
import { useTranslation } from "react-i18next";
import Widget from "../widget";
import Block from "../block";
import { formatApiUrl } 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"));
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>
);
}
const have = booksData.filter((book) => book.statistics.bookFileCount > 0);
return (
<Widget>
<Block label={t("readarr.wanted")} value={wantedData.totalRecords} />
<Block label={t("readarr.queued")} value={queueData.totalCount} />
<Block label={t("readarr.books")} value={have.length} />
</Widget>
);
}

@ -6,22 +6,22 @@ import Block from "../block";
import { formatApiUrl } from "utils/api-helpers";
export default function Sabnzbd({ service }) {
const { t } = useTranslation("common");
export default function SABnzbd({ service }) {
const { t } = useTranslation();
const config = service.widget;
const { data: statusData, error: statusError } = useSWR(formatApiUrl(config, "mode=queue"));
if (statusError) {
const { data: queueData, error: queueError } = useSWR(formatApiUrl(config, "queue"));
if (queueError) {
return <Widget error={t("widget.api_error")} />;
}
if (!statusData) {
if (!queueData) {
return (
<Widget>
<Block label={t("sabnzbd.status")} />
<Block label={t("sabnzbd.speed")} />
<Block label={t("sabnzbd.remaining")} />
<Block label={t("sabnzbd.rate")} />
<Block label={t("sabnzbd.queue")} />
<Block label={t("sabnzbd.timeleft")} />
</Widget>
);
@ -29,13 +29,9 @@ export default function Sabnzbd({ service }) {
return (
<Widget>
<Block label={t("sabnzbd.status")} value={statusData?.queue?.status } />
<Block label={t("sabnzbd.speed")} value={statusData?.queue?.speed } />
<Block
label={t("sabnzbd.remaining")}
value={t("common.bytes", { value: statusData?.queue?.mbleft * 1024 * 1024 })}
/>
<Block label={t("sabnzbd.timeleft")} value={statusData?.queue?.timeleft} />
<Block label={t("sabnzbd.rate")} value={`${queueData.queue.speed}bps`} />
<Block label={t("sabnzbd.queue")} value={queueData.queue.noofslots} />
<Block label={t("sabnzbd.timeleft")} value={queueData.queue.timeleft} />
</Widget>
);
}

@ -104,7 +104,7 @@ export default function Tautulli({ service }) {
if (!activityData) {
return (
<div className="flex flex-col pb-1">
<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>
@ -127,7 +127,7 @@ export default function Tautulli({ service }) {
if (playing.length === 0) {
return (
<div className="flex flex-col pb-1">
<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>
@ -141,14 +141,14 @@ export default function Tautulli({ service }) {
if (playing.length === 1) {
const session = playing[0];
return (
<div className="flex flex-col pb-1">
<div className="flex flex-col pb-1 mx-1">
<SingleSessionEntry session={session} />
</div>
);
}
return (
<div className="flex flex-col pb-1">
<div className="flex flex-col pb-1 mx-1">
{playing.map((session) => (
<SessionEntry key={session.Id} session={session} />
))}

@ -16,7 +16,7 @@ function Widget({ options }) {
if (error || data?.cod === 401 || data?.error) {
return (
<div className="flex flex-col justify-center">
<div className="flex flex-col justify-center first:ml-0 ml-4">
<div className="flex flex-row items-center justify-end">
<div className="flex flex-col items-center">
<BiError className="w-8 h-8 text-theme-800 dark:text-theme-200" />
@ -32,7 +32,7 @@ function Widget({ options }) {
if (!data) {
return (
<div className="flex flex-col justify-center">
<div className="flex flex-col justify-center first:ml-0 ml-4">
<div className="flex flex-row items-center justify-end">
<div className="flex flex-col items-center">
<WiCloudDown className="w-8 h-8 text-theme-800 dark:text-theme-200" />
@ -49,7 +49,7 @@ function Widget({ options }) {
const unit = options.units === "metric" ? "celsius" : "fahrenheit";
return (
<div className="flex flex-col justify-center">
<div className="flex flex-col justify-center first:ml-0 ml-4">
<div className="flex flex-row items-center justify-end">
<div className="flex flex-col items-center">
<Icon
@ -100,7 +100,7 @@ export default function OpenWeatherMap({ options }) {
if (!location) {
return (
<button type="button" onClick={() => requestLocation()} className="flex flex-col justify-center">
<button type="button" onClick={() => requestLocation()} className="flex flex-col justify-center first:ml-0 ml-4">
<div className="flex flex-row items-center justify-end">
<div className="flex flex-col items-center">
{requesting ? (

@ -51,7 +51,7 @@ export default function Search({ options }) {
}
return (
<form className="flex-col relative h-8 my-4 min-w-full md:min-w-fit grow mr-4" onSubmit={handleSubmit}>
<form className="flex-col relative h-8 my-4 min-w-full md:min-w-fit grow first:ml-0 ml-4" onSubmit={handleSubmit}>
<div className="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none w-full text-theme-800 dark:text-white" />
<input
type="text"

@ -16,7 +16,7 @@ function Widget({ options }) {
if (error || data?.error) {
return (
<div className="flex flex-col justify-center">
<div className="flex flex-col justify-center first:ml-0 ml-4">
<div className="flex flex-row items-center justify-end">
<div className="flex flex-col items-center">
<BiError className="w-8 h-8 text-theme-800 dark:text-theme-200" />
@ -32,7 +32,7 @@ function Widget({ options }) {
if (!data) {
return (
<div className="flex flex-col justify-center">
<div className="flex flex-col justify-center first:ml-0 ml-4">
<div className="flex flex-row items-center justify-end">
<div className="flex flex-col items-center">
<WiCloudDown className="w-8 h-8 text-theme-800 dark:text-theme-200" />
@ -49,7 +49,7 @@ function Widget({ options }) {
const unit = options.units === "metric" ? "celsius" : "fahrenheit";
return (
<div className="flex flex-col justify-center">
<div className="flex flex-col justify-center first:ml-0 ml-4">
<div className="flex flex-row items-center justify-end">
<div className="flex flex-col items-center">
<Icon condition={data.current.condition.code} timeOfDay={data.current.is_day ? "day" : "night"} />
@ -101,7 +101,7 @@ export default function WeatherApi({ options }) {
if (!location) {
return (
<button type="button" onClick={() => requestLocation()} className="flex flex-col justify-center">
<button type="button" onClick={() => requestLocation()} className="flex flex-col justify-center first:ml-0 ml-4">
<div className="flex flex-row items-center justify-end">
<div className="flex flex-col items-center">
{requesting ? (

@ -11,16 +11,18 @@ const serviceProxyHandlers = {
pihole: genericProxyHandler,
radarr: genericProxyHandler,
sonarr: genericProxyHandler,
readarr: genericProxyHandler,
speedtest: genericProxyHandler,
tautulli: genericProxyHandler,
traefik: genericProxyHandler,
sabnzbd: genericProxyHandler,
// uses X-API-Key header auth
// uses X-API-Key (or similar) header auth
gotify: credentialedProxyHandler,
portainer: credentialedProxyHandler,
jellyseerr: credentialedProxyHandler,
overseerr: credentialedProxyHandler,
ombi: credentialedProxyHandler,
coinmarketcap: credentialedProxyHandler,
// super specific handlers
rutorrent: rutorrentProxyHandler,
nzbget: nzbgetProxyHandler,

@ -13,8 +13,10 @@ const formats = {
overseerr: `{url}/api/v1/{endpoint}`,
ombi: `{url}/api/v1/{endpoint}`,
npm: `{url}/api/{endpoint}`,
readarr: `{url}/api/v1/{endpoint}?apikey={key}`,
sabnzbd: `{url}/api/?apikey={key}&output=json&mode={endpoint}`,
coinmarketcap: `{url}/{endpoint}`,
gotify: `{url}/{endpoint}`,
sabnzbd: `{url}/api?output=json&apikey={key}&{endpoint}`,
};
export function formatApiCall(api, args) {

@ -17,11 +17,22 @@ export default async function credentialedProxyHandler(req, res) {
if (widget) {
const url = new URL(formatApiCall(widget.type, { endpoint, ...widget }));
const headers = {
"Content-Type": "application/json",
};
if (widget.type === "coinmarketcap") {
headers["X-CMC_PRO_API_KEY"] = `${widget.key}`;
} else {
headers["X-API-Key"] = `${widget.key}`;
}
const [status, contentType, data] = await httpProxy(url, {
method: req.method,
withCredentials: true,
credentials: "include",
headers: headersData,
headers,
});
if (status === 204 || status === 304) {

@ -110,13 +110,18 @@ export function cleanServiceGroups(groups) {
const cleanedService = { ...service };
if (cleanedService.widget) {
const { type } = cleanedService.widget;
const { type, server, container } = cleanedService.widget;
cleanedService.widget = {
type,
service_name: service.name,
service_group: serviceGroup.name,
};
if (type === "docker") {
cleanedService.widget.server = server;
cleanedService.widget.container = container;
}
}
return cleanedService;

Loading…
Cancel
Save