Merge branch 'benphelps:main' into main

This commit is contained in:
GodD6366 2022-10-12 22:46:59 +08:00 committed by GitHub
commit 5644cf89ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 828 additions and 56 deletions

View File

@ -32,7 +32,8 @@
"shvl": "^3.0.0",
"swr": "^1.3.0",
"tough-cookie": "^4.1.2",
"winston": "^3.8.2"
"winston": "^3.8.2",
"xml-js": "^1.6.11"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.3",

View File

@ -40,6 +40,7 @@ specifiers:
tough-cookie: ^4.1.2
typescript: ^4.8.3
winston: ^3.8.2
xml-js: ^1.6.11
dependencies:
'@headlessui/react': 1.7.2_biqbaboplfbrettd7655fr4n2y
@ -65,6 +66,7 @@ dependencies:
swr: 1.3.0_react@18.2.0
tough-cookie: 4.1.2
winston: 3.8.2
xml-js: 1.6.11
devDependencies:
'@tailwindcss/forms': 0.5.3_tailwindcss@3.1.8
@ -2554,6 +2556,10 @@ packages:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
dev: false
/sax/1.2.4:
resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==}
dev: false
/scheduler/0.23.0:
resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==}
dependencies:
@ -3000,6 +3006,13 @@ packages:
/wrappy/1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
/xml-js/1.6.11:
resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==}
hasBin: true
dependencies:
sax: 1.2.4
dev: false
/xtend/4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}

View File

@ -0,0 +1,205 @@
{
"sabnzbd": {
"queue": "Опашка",
"timeleft": "Оставащо Време",
"rate": "Rate"
},
"rutorrent": {
"active": "Акитивен",
"upload": "Споделяне",
"download": "Сваляне"
},
"widget": {
"missing_type": "Липсваща приставка: {{type}}",
"api_error": "API Грешка",
"status": "Статус"
},
"weather": {
"current": "Текущо местоположение",
"allow": "Разреши",
"updating": "Обновяване",
"wait": "Моля изчакайте"
},
"search": {
"placeholder": "Търсене…"
},
"resources": {
"cpu": "CPU",
"total": "Общо",
"free": "Свободни",
"used": "Заети",
"load": "Натоварване"
},
"unifi": {
"users": "Потребители",
"uptime": "Активен от",
"days": "Дни",
"wan": "WAN",
"lan_users": "LAN Потребители",
"wlan_users": "WLAN Потребители",
"up": "UP",
"down": "DOWN",
"wait": "Моля изчакайте"
},
"docker": {
"offline": "Изключен",
"rx": "RX",
"tx": "TX",
"mem": "MEM",
"cpu": "CPU"
},
"emby": {
"playing": "Възпроизвежда",
"transcoding": "Конвертира",
"bitrate": "Bitrate",
"no_active": "Няма активни потоци"
},
"tautulli": {
"playing": "Възпроизвежда",
"transcoding": "Конвертира",
"bitrate": "Честота",
"no_active": "Няма активни потоци"
},
"nzbget": {
"rate": "Rate",
"remaining": "Остава",
"downloaded": "Изтеглени"
},
"plex": {
"streams": "Активни Потоци",
"movies": "Филми",
"tv": "Сериали"
},
"transmission": {
"download": "Сваляне",
"upload": "Upload",
"leech": "Leech",
"seed": "Seed"
},
"qbittorrent": {
"download": "Download",
"upload": "Upload",
"leech": "Leech",
"seed": "Seed"
},
"sonarr": {
"wanted": "Wanted",
"queued": "Queued",
"series": "Series"
},
"radarr": {
"wanted": "Wanted",
"queued": "Queued",
"movies": "Movies"
},
"lidarr": {
"wanted": "Wanted",
"queued": "Queued",
"albums": "Albums"
},
"readarr": {
"wanted": "Wanted",
"queued": "Queued",
"books": "Books"
},
"bazarr": {
"missingEpisodes": "Missing Episodes",
"missingMovies": "Missing Movies"
},
"ombi": {
"pending": "Pending",
"approved": "Approved",
"available": "Available"
},
"jellyseerr": {
"pending": "Pending",
"approved": "Approved",
"available": "Available"
},
"overseerr": {
"pending": "Pending",
"approved": "Approved",
"available": "Available"
},
"pihole": {
"queries": "Queries",
"blocked": "Blocked",
"gravity": "Gravity"
},
"adguard": {
"queries": "Queries",
"blocked": "Blocked",
"filtered": "Filtered",
"latency": "Latency"
},
"speedtest": {
"upload": "Upload",
"download": "Download",
"ping": "Ping"
},
"portainer": {
"running": "Running",
"stopped": "Stopped",
"total": "Total"
},
"traefik": {
"routers": "Routers",
"services": "Services",
"middleware": "Middleware"
},
"npm": {
"enabled": "Enabled",
"disabled": "Disabled",
"total": "Total"
},
"coinmarketcap": {
"configure": "Configure one or more crypto currencies to track",
"1hour": "1 Hour",
"1day": "1 Day",
"7days": "7 Days",
"30days": "30 Days"
},
"gotify": {
"apps": "Applications",
"clients": "Clients",
"messages": "Messages"
},
"prowlarr": {
"enableIndexers": "Indexers",
"numberOfGrabs": "Grabs",
"numberOfQueries": "Queries",
"numberOfFailGrabs": "Fail Grabs",
"numberOfFailQueries": "Fail Queries"
},
"authentik": {
"loginsLast24H": "Logins (24h)",
"users": "Users",
"failedLoginsLast24H": "Failed Logins (24h)"
},
"jackett": {
"configured": "Configured",
"errored": "Errored"
},
"strelaysrv": {
"numActiveSessions": "Sessions",
"numConnections": "Connections",
"dataRelayed": "Relayed",
"transferRate": "Rate"
},
"mastodon": {
"user_count": "Users",
"status_count": "Posts",
"domain_count": "Domains"
},
"proxmox": {
"mem": "MEM",
"cpu": "CPU",
"lxc": "LXC",
"vms": "VMs"
},
"glances": {
"cpu": "CPU",
"mem": "MEM",
"wait": "Please wait"
}
}

View File

@ -191,5 +191,15 @@
"up": "UP",
"down": "DOWN",
"wait": "Please wait"
},
"plex": {
"streams": "Active Streams",
"movies": "Movies",
"tv": "TV Shows"
},
"glances": {
"cpu": "CPU",
"mem": "MEM",
"wait": "Please wait"
}
}

View File

@ -191,5 +191,15 @@
"up": "UP",
"down": "DOWN",
"wait": "Please wait"
},
"plex": {
"streams": "Active Streams",
"movies": "Movies",
"tv": "TV Shows"
},
"glances": {
"cpu": "CPU",
"mem": "MEM",
"wait": "Please wait"
}
}

View File

@ -66,6 +66,11 @@
"remaining": "Remaining",
"downloaded": "Downloaded"
},
"plex": {
"streams": "Active Streams",
"movies": "Movies",
"tv": "TV Shows"
},
"sabnzbd": {
"rate": "Rate",
"queue": "Queue",
@ -202,5 +207,10 @@
"cpu": "CPU",
"lxc": "LXC",
"vms": "VMs"
},
"glances": {
"cpu": "CPU",
"mem": "MEM",
"wait": "Please wait"
}
}

View File

@ -191,5 +191,15 @@
"wlan_users": "WLAN Users",
"down": "DOWN",
"wait": "Please wait"
},
"plex": {
"streams": "Active Streams",
"movies": "Movies",
"tv": "TV Shows"
},
"glances": {
"cpu": "CPU",
"mem": "MEM",
"wait": "Please wait"
}
}

View File

@ -191,5 +191,15 @@
"wan": "WAN",
"up": "UP",
"down": "DOWN"
},
"plex": {
"streams": "Active Streams",
"movies": "Movies",
"tv": "TV Shows"
},
"glances": {
"wait": "Please wait",
"cpu": "CPU",
"mem": "MEM"
}
}

View File

@ -182,14 +182,24 @@
"vms": "VMs"
},
"unifi": {
"users": "Users",
"uptime": "System Uptime",
"days": "Days",
"users": "Utilisateurs",
"uptime": "Disponibilité du système",
"days": "Jours",
"wan": "WAN",
"lan_users": "LAN Users",
"wlan_users": "WLAN Users",
"lan_users": "Utilisateurs LAN",
"wlan_users": "Utilisateurs WLAN",
"up": "UP",
"down": "DOWN",
"wait": "Merci de patienter"
},
"plex": {
"streams": "Flux actif",
"movies": "Films",
"tv": "Séries TV"
},
"glances": {
"cpu": "CPU",
"mem": "MEM",
"wait": "Please wait"
}
}

View File

@ -191,5 +191,15 @@
"up": "UP",
"down": "DOWN",
"wait": "Please wait"
},
"plex": {
"streams": "Active Streams",
"movies": "Movies",
"tv": "TV Shows"
},
"glances": {
"cpu": "CPU",
"mem": "MEM",
"wait": "Please wait"
}
}

View File

@ -191,5 +191,15 @@
"up": "UP",
"down": "DOWN",
"wait": "Please wait"
},
"plex": {
"streams": "Active Streams",
"movies": "Movies",
"tv": "TV Shows"
},
"glances": {
"cpu": "CPU",
"mem": "MEM",
"wait": "Please wait"
}
}

View File

@ -191,5 +191,15 @@
"up": "UP",
"down": "DOWN",
"wait": "Please wait"
},
"plex": {
"streams": "Active Streams",
"movies": "Movies",
"tv": "TV Shows"
},
"glances": {
"cpu": "CPU",
"mem": "MEM",
"wait": "Please wait"
}
}

View File

@ -191,5 +191,15 @@
"wlan_users": "WLAN Users",
"up": "UP",
"down": "DOWN"
},
"plex": {
"streams": "Active Streams",
"movies": "Movies",
"tv": "TV Shows"
},
"glances": {
"cpu": "CPU",
"mem": "MEM",
"wait": "Please wait"
}
}

View File

@ -191,5 +191,15 @@
"up": "UP",
"down": "DOWN",
"wait": "Please wait"
},
"plex": {
"streams": "Active Streams",
"movies": "Movies",
"tv": "TV Shows"
},
"glances": {
"cpu": "CPU",
"mem": "MEM",
"wait": "Please wait"
}
}

View File

@ -191,5 +191,15 @@
"up": "UP",
"down": "DOWN",
"wait": "Please wait"
},
"plex": {
"streams": "Active Streams",
"movies": "Movies",
"tv": "TV Shows"
},
"glances": {
"cpu": "CPU",
"mem": "MEM",
"wait": "Please wait"
}
}

View File

@ -191,5 +191,15 @@
"up": "UP",
"down": "DOWN",
"wait": "Please wait"
},
"plex": {
"streams": "Active Streams",
"movies": "Movies",
"tv": "TV Shows"
},
"glances": {
"cpu": "CPU",
"mem": "MEM",
"wait": "Please wait"
}
}

View File

@ -191,5 +191,15 @@
"up": "UP",
"down": "DOWN",
"wait": "Please wait"
},
"plex": {
"streams": "Active Streams",
"movies": "Movies",
"tv": "TV Shows"
},
"glances": {
"cpu": "CPU",
"mem": "MEM",
"wait": "Please wait"
}
}

View File

@ -202,5 +202,15 @@
"up": "UP",
"down": "DOWN",
"wait": "Please wait"
},
"plex": {
"streams": "Active Streams",
"movies": "Movies",
"tv": "TV Shows"
},
"glances": {
"cpu": "CPU",
"mem": "MEM",
"wait": "Please wait"
}
}

View File

@ -191,5 +191,15 @@
"up": "UP",
"down": "DOWN",
"wait": "Please wait"
},
"plex": {
"streams": "Active Streams",
"movies": "Movies",
"tv": "TV Shows"
},
"glances": {
"cpu": "CPU",
"mem": "MEM",
"wait": "Please wait"
}
}

View File

@ -191,5 +191,15 @@
"up": "UP",
"down": "DOWN",
"wait": "Please wait"
},
"plex": {
"streams": "Active Streams",
"movies": "Movies",
"tv": "TV Shows"
},
"glances": {
"cpu": "CPU",
"mem": "MEM",
"wait": "Please wait"
}
}

View File

@ -191,5 +191,15 @@
"up": "UP",
"down": "DOWN",
"wait": "Please wait"
},
"plex": {
"streams": "Active Streams",
"movies": "Movies",
"tv": "TV Shows"
},
"glances": {
"cpu": "CPU",
"mem": "MEM",
"wait": "Please wait"
}
}

View File

@ -31,9 +31,9 @@
"placeholder": "వెతకండి…"
},
"resources": {
"cpu": "సీ పి యూ",
"cpu": "సీపియూ",
"total": "మొత్తం",
"free": "ఉచిత",
"free": "మిగిలింది",
"used": "ఉపయోగించబడిన",
"load": "లోడ్"
},
@ -41,7 +41,7 @@
"rx": "RX",
"tx": "TX",
"mem": "MEM",
"cpu": "CPU",
"cpu": "సీపియూ",
"offline": "ఆఫ్‌లైన్"
},
"emby": {
@ -128,8 +128,8 @@
"ping": "పింగ్"
},
"portainer": {
"running": "నడుస్తోంది",
"stopped": "ఆగిపోయింది",
"running": "నడుస్తున్నవి",
"stopped": "ఆగిపోయినవి",
"total": "మొత్తం"
},
"traefik": {
@ -139,7 +139,7 @@
},
"npm": {
"enabled": "ప్రారంభించబడింది",
"disabled": "Disabled",
"disabled": "డిసేబ్లెడ్",
"total": "మొత్తం"
},
"coinmarketcap": {
@ -177,19 +177,29 @@
},
"proxmox": {
"mem": "MEM",
"cpu": "CPU",
"cpu": "సీపియూ",
"lxc": "LXC",
"vms": "VMs"
"vms": "విఎంలు"
},
"unifi": {
"users": "Users",
"uptime": "System Uptime",
"days": "Days",
"users": "వినియోగదారులు",
"uptime": "సిస్టమ్ సమయము",
"days": "రోజులు",
"wan": "WAN",
"lan_users": "LAN Users",
"wlan_users": "WLAN Users",
"up": "UP",
"down": "DOWN",
"lan_users": "LAN వినియోగదారులు",
"wlan_users": "WLAN వినియోగదారులు",
"up": "అప్",
"down": "డౌన్",
"wait": "దయచేసి వేచి ఉండండి"
},
"plex": {
"streams": "యాక్టివ్ స్ట్రీమ్‌లు",
"movies": "సినిమాలు",
"tv": "దూరదర్శిని కార్యక్రమాలు"
},
"glances": {
"cpu": "CPU",
"mem": "MEM",
"wait": "Please wait"
}
}

View File

@ -191,5 +191,15 @@
"up": "UP",
"down": "DOWN",
"wait": "Please wait"
},
"plex": {
"streams": "Active Streams",
"movies": "Movies",
"tv": "TV Shows"
},
"glances": {
"cpu": "CPU",
"mem": "MEM",
"wait": "Please wait"
}
}

View File

@ -191,5 +191,15 @@
"up": "UP",
"down": "DOWN",
"wait": "Please wait"
},
"plex": {
"streams": "Active Streams",
"movies": "Movies",
"tv": "TV Shows"
},
"glances": {
"cpu": "CPU",
"mem": "MEM",
"wait": "Please wait"
}
}

View File

@ -92,7 +92,7 @@
"current": "当前定位",
"allow": "点击并允许",
"updating": "更新中",
"wait": "请稍"
"wait": "请稍"
},
"overseerr": {
"pending": "待办",
@ -190,6 +190,16 @@
"wlan_users": "WLAN Users",
"up": "UP",
"down": "DOWN",
"wait": "请稍候"
},
"plex": {
"streams": "Active Streams",
"movies": "Movies",
"tv": "TV Shows"
},
"glances": {
"cpu": "CPU",
"mem": "MEM",
"wait": "Please wait"
}
}

View File

@ -191,5 +191,15 @@
"up": "UP",
"down": "DOWN",
"wait": "Please wait"
},
"plex": {
"streams": "Active Streams",
"movies": "Movies",
"tv": "TV Shows"
},
"glances": {
"cpu": "CPU",
"mem": "MEM",
"wait": "Please wait"
}
}

View File

@ -9,19 +9,37 @@ import Docker from "widgets/docker/component";
import { SettingsContext } from "utils/contexts/settings";
function resolveIcon(icon) {
if (icon.startsWith("http")) {
return icon;
// direct or relative URLs
if (icon.startsWith("http") || icon.startsWith("/")) {
return <Image src={`${icon}`} width={32} height={32} alt="logo" />;
}
if (icon.startsWith("/")) {
return icon;
// mdi- prefixed, material design icons
if (icon.startsWith("mdi-")) {
const iconName = icon.replace("mdi-", "").replace(".svg", "");
return (
<div
style={{
width: 32,
height: 32,
background: "linear-gradient(180deg, rgb(var(--color-logo-start)), rgb(var(--color-logo-stop)))",
mask: `url(https://cdn.jsdelivr.net/npm/@mdi/svg@latest/svg/${iconName}.svg) no-repeat center / contain`,
WebkitMask: `url(https://cdn.jsdelivr.net/npm/@mdi/svg@latest/svg/${iconName}.svg) no-repeat center / contain`,
}}
/>
);
}
if (icon.endsWith(".png")) {
return `https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${icon}`;
}
return `https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${icon}.png`;
// fallback to dashboard-icons
const iconName = icon.replace(".png", "");
return (
<Image
src={`https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${iconName}.png`}
width={32}
height={32}
alt="logo"
/>
);
}
export default function Item({ service }) {
@ -57,12 +75,10 @@ export default function Item({ service }) {
rel="noreferrer"
className="flex-shrink-0 flex items-center justify-center w-12 "
>
<Image src={resolveIcon(service.icon)} width={32} height={32} alt="logo" />
{resolveIcon(service.icon)}
</a>
) : (
<div className="flex-shrink-0 flex items-center justify-center w-12 ">
<Image src={resolveIcon(service.icon)} width={32} height={32} alt="logo" />
</div>
<div className="flex-shrink-0 flex items-center justify-center w-12 ">{resolveIcon(service.icon)}</div>
))}
{hasLink ? (

View File

@ -0,0 +1,110 @@
import useSWR from "swr";
import { BiError } from "react-icons/bi";
import { FaMemory } from "react-icons/fa";
import { FiCpu } from "react-icons/fi";
import { useTranslation } from "next-i18next";
import UsageBar from "../resources/usage-bar";
export default function Widget({ options }) {
const { t, i18n } = useTranslation();
const { data, error } = useSWR(
`/api/widgets/glances?${new URLSearchParams({ lang: i18n.language, ...options }).toString()}`, {
refreshInterval: 1500,
}
);
if (error || data?.error) {
return (
<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-row items-center">
<BiError className="w-8 h-8 text-theme-800 dark:text-theme-200" />
<div className="flex flex-col ml-3 text-left">
<span className="text-theme-800 dark:text-theme-200 text-sm">{t("widget.api_error")}</span>
</div>
</div>
</div>
</div>
);
}
if (!data) {
return (
<div className="flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap ml-4">
<div className="flex flex-row self-center flex-wrap justify-between">
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
<FiCpu className="text-theme-800 dark:text-theme-200 w-5 h-5" />
<div className="flex flex-col ml-3 text-left min-w-[85px]">
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
<div className="pl-0.5 text-xs">
{t("glances.wait")}
</div>
</div>
<UsageBar percent="0" />
</div>
</div>
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
<FaMemory className="text-theme-800 dark:text-theme-200 w-5 h-5" />
<div className="flex flex-col ml-3 text-left min-w-[85px]">
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
<div className="pl-0.5 text-xs">
{t("glances.wait")}
</div>
</div>
<UsageBar percent="0" />
</div>
</div>
</div>
{options.label && (
<div className="ml-6 pt-1 text-center text-theme-800 dark:text-theme-200 text-xs">{options.label}</div>
)}
</div>
);
}
return (
<div className="flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap ml-4">
<div className="flex flex-row self-center flex-wrap justify-between">
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
<FiCpu className="text-theme-800 dark:text-theme-200 w-5 h-5" />
<div className="flex flex-col ml-3 text-left min-w-[85px]">
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
<div className="pl-0.5">
{t("common.number", {
value: data.cpu,
style: "unit",
unit: "percent",
maximumFractionDigits: 0,
})}
</div>
<div className="pr-1">{t("glances.cpu")}</div>
</div>
<UsageBar percent={data.cpu} />
</div>
</div>
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
<FaMemory className="text-theme-800 dark:text-theme-200 w-5 h-5" />
<div className="flex flex-col ml-3 text-left min-w-[85px]">
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
<div className="pl-0.5">
{t("common.number", {
value: data.mem,
style: "unit",
unit: "percent",
maximumFractionDigits: 0,
})}
</div>
<div className="pr-1">{t("glances.mem")}</div>
</div>
<UsageBar percent={data.mem} />
</div>
</div>
</div>
{options.label && (
<div className="pt-1 text-center text-theme-800 dark:text-theme-200 text-xs">{options.label}</div>
)}
</div>
);
}

View File

@ -11,6 +11,7 @@ const widgetMappings = {
datetime: dynamic(() => import("components/widgets/datetime/datetime")),
logo: dynamic(() => import("components/widgets/logo/logo"), { ssr: false }),
unifi_console: dynamic(() => import("components/widgets/unifi_console/unifi_console")),
glances: dynamic(() => import("components/widgets/glances/glances")),
};
export default function Widget({ widget }) {

View File

@ -0,0 +1,52 @@
import { httpProxy } from "utils/proxy/http";
import createLogger from "utils/logger";
import { getSettings } from "utils/config/config";
const logger = createLogger("glances");
export default async function handler(req, res) {
const { id } = req.query;
let errorMessage;
let instanceID = "glances";
if (id) { // multiple instances
instanceID = id;
}
const settings = getSettings();
const instanceSettings = settings[instanceID];
if (!instanceSettings) {
errorMessage = id ? `There is no glances section with id '${id}' in settings.yaml` : "There is no glances section in settings.yaml";
logger.error(errorMessage);
return res.status(400).json({ error: errorMessage });
}
const url = instanceSettings?.url;
if (!url) {
errorMessage = "Missing Glances URL";
logger.error(errorMessage);
return res.status(400).json({ error: errorMessage });
}
const apiUrl = `${url}/api/3/quicklook`;
const headers = {
"Accept-Encoding": "application/json"
};
if (instanceSettings.username && instanceSettings.password) {
headers.Authorization = `Basic ${Buffer.from(`${instanceSettings.username}:${instanceSettings.password}`).toString("base64")}`
}
const params = { method: "GET", headers };
const [status, contentType, data] = await httpProxy(apiUrl, params);
if (status === 401) {
logger.error("Authorization failure getting data from glances API. Data: %s", data);
}
if (status !== 200) {
logger.error("HTTP %d getting data from glances API. Data: %s", status, data);
}
if (contentType) res.setHeader("Content-Type", contentType);
return res.status(status).send(data);
}

View File

@ -1,33 +1,13 @@
import checkAndCopyConfig, { getSettings } from "utils/config/config";
import themes from "utils/styles/themes";
import { servicesResponse, bookmarksResponse } from "utils/config/api-response";
export async function getServerSideProps({ res }) {
checkAndCopyConfig("settings.yaml");
const settings = getSettings();
const services = await servicesResponse();
const bookmarks = await bookmarksResponse();
const color = settings.color || "slate";
const theme = settings.theme || "dark";
const serviceShortcuts = services.map((group) =>
group.services.map((service) => ({
name: service.name,
url: service.href,
description: service.description,
}))
);
const bookmarkShortcuts = bookmarks.map((group) =>
group.bookmarks.map((service) => ({
name: service.name,
url: service.href,
}))
);
const shortcuts = [...serviceShortcuts, ...bookmarkShortcuts].flat();
const manifest = {
name: settings.title || "Homepage",
short_name: settings.title || "Homepage",
@ -43,7 +23,6 @@ export async function getServerSideProps({ res }) {
type: "image/png",
},
],
shortcuts,
theme_color: themes[color][theme],
background_color: themes[color][theme],
display: "standalone",

View File

@ -18,6 +18,7 @@ const components = {
ombi: dynamic(() => import("./ombi/component")),
overseerr: dynamic(() => import("./overseerr/component")),
pihole: dynamic(() => import("./pihole/component")),
plex: dynamic(() => import("./plex/component")),
portainer: dynamic(() => import("./portainer/component")),
prowlarr: dynamic(() => import("./prowlarr/component")),
proxmox: dynamic(() => import("./proxmox/component")),

View File

@ -0,0 +1,38 @@
import useSWR from "swr";
import { useTranslation } from "next-i18next";
import Block from "components/services/widget/block";
import Container from "components/services/widget/container";
import { formatProxyUrl } from "utils/proxy/api-helpers";
export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service;
const { data: plexData, error: plexAPIError } = useSWR(formatProxyUrl(widget, "unified"), {
refreshInterval: 5000,
});
if (plexAPIError) {
return <Container error={t("widget.api_error")} />;
}
if (!plexData) {
return (
<Container service={service}>
<Block label="plex.streams" />
<Block label="plex.movies" />
<Block label="plex.tv" />
</Container>
);
}
return (
<Container service={service}>
<Block label="plex.streams" value={t("common.number", { value: plexData.streams })} />
<Block label="plex.movies" value={t("common.number", { value: plexData.movies })} />
<Block label="plex.tv" value={t("common.number", { value: plexData.tv })} />
</Container>
);
}

110
src/widgets/plex/proxy.js Normal file
View File

@ -0,0 +1,110 @@
/* eslint-disable no-underscore-dangle */
import cache from "memory-cache";
import { xml2json } from "xml-js";
import { formatApiCall } from "utils/proxy/api-helpers";
import { httpProxy } from "utils/proxy/http";
import getServiceWidget from "utils/config/service-helpers";
import createLogger from "utils/logger";
import widgets from "widgets/widgets";
const proxyName = "plexProxyHandler";
const librariesCacheKey = `${proxyName}__libraries`;
const moviesCacheKey = `${proxyName}__movies`;
const tvCacheKey = `${proxyName}__tv`;
const logger = createLogger(proxyName);
async function getWidget(req) {
const { group, service } = req.query;
if (!group || !service) {
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
return null;
}
const widget = await getServiceWidget(group, service);
if (!widget) {
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
return null;
}
return widget;
}
async function fetchFromPlexAPI(endpoint, widget) {
const api = widgets?.[widget.type]?.api;
if (!api) {
return [403, null];
}
const url = new URL(formatApiCall(api, { endpoint, ...widget }));
const [status, contentType, data] = await httpProxy(url);
if (status !== 200) {
logger.error("HTTP %d communicating with Plex. Data: %s", status, data.toString());
return [status, data.toString()];
}
try {
const dataDecoded = xml2json(data.toString(), { compact: true });
return [status, JSON.parse(dataDecoded), contentType];
} catch (e) {
logger.error("Error decoding Plex API data. Data: %s", data.toString());
return [status, null];
}
}
export default async function plexProxyHandler(req, res) {
const widget = await getWidget(req);
if (!widget) {
return res.status(400).json({ error: "Invalid proxy service type" });
}
logger.debug("Getting streams from Plex API");
let streams;
let [status, apiData] = await fetchFromPlexAPI("/status/sessions", widget);
if (apiData && apiData.MediaContainer) {
streams = apiData.MediaContainer._attributes.size;
}
let libraries = cache.get(librariesCacheKey);
if (libraries === null) {
logger.debug("Getting libraries from Plex API");
[status, apiData] = await fetchFromPlexAPI("/library/sections", widget);
if (apiData && apiData.MediaContainer) {
libraries = apiData.MediaContainer.Directory;
cache.put(librariesCacheKey, libraries, 1000 * 60 * 60 * 6);
}
}
let movies = cache.get(moviesCacheKey);
let tv = cache.get(tvCacheKey);
if (movies === null || tv === null) {
movies = 0;
tv = 0;
logger.debug("Getting movie + tv counts from Plex API");
libraries.filter(l => ["movie", "show"].includes(l._attributes.type)).forEach(async (library) => {
[status, apiData] = await fetchFromPlexAPI(`/library/sections/${library._attributes.key}/all`, widget);
if (apiData && apiData.MediaContainer) {
const size = parseInt(apiData.MediaContainer._attributes.size, 10);
if (library._attributes.type === "movie") {
movies += size;
} else if (library._attributes.type === "show") {
tv += size;
}
}
cache.put(tvCacheKey, tv, 1000 * 60 * 10);
cache.put(moviesCacheKey, movies, 1000 * 60 * 10);
});
}
const data = {
streams,
tv,
movies
};
return res.status(status).send(data);
}

View File

@ -0,0 +1,14 @@
import plexProxyHandler from "./proxy";
const widget = {
api: "{url}{endpoint}?X-Plex-Token={key}",
proxyHandler: plexProxyHandler,
mappings: {
unified: {
endpoint: "/",
},
},
};
export default widget;

View File

@ -13,6 +13,7 @@ import nzbget from "./nzbget/widget";
import ombi from "./ombi/widget";
import overseerr from "./overseerr/widget";
import pihole from "./pihole/widget";
import plex from "./plex/widget";
import portainer from "./portainer/widget";
import prowlarr from "./prowlarr/widget";
import proxmox from "./proxmox/widget";
@ -46,6 +47,7 @@ const widgets = {
ombi,
overseerr,
pihole,
plex,
portainer,
prowlarr,
proxmox,