Merge branch 'benphelps:main' into main
This commit is contained in:
commit
5644cf89ab
@ -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",
|
||||
|
||||
@ -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'}
|
||||
|
||||
205
public/locales/bg/common.json
Normal file
205
public/locales/bg/common.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 ? (
|
||||
|
||||
110
src/components/widgets/glances/glances.jsx
Normal file
110
src/components/widgets/glances/glances.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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 }) {
|
||||
|
||||
52
src/pages/api/widgets/glances.js
Normal file
52
src/pages/api/widgets/glances.js
Normal 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);
|
||||
}
|
||||
@ -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",
|
||||
|
||||
@ -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")),
|
||||
|
||||
38
src/widgets/plex/component.jsx
Normal file
38
src/widgets/plex/component.jsx
Normal 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
110
src/widgets/plex/proxy.js
Normal 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);
|
||||
}
|
||||
14
src/widgets/plex/widget.js
Normal file
14
src/widgets/plex/widget.js
Normal 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;
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user