mirror of
https://github.com/gethomepage/homepage.git
synced 2025-12-07 09:35:54 -08:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a226ca473 | ||
|
|
33e6d54fd2 | ||
|
|
d36f37a4ed | ||
|
|
f3ebbb6547 | ||
|
|
28b2f79e5b | ||
|
|
9a77115a30 | ||
|
|
2d899e364d | ||
|
|
32b881891c | ||
|
|
9eefc07c7c | ||
|
|
792accffb6 | ||
|
|
03af88aba5 | ||
|
|
f56b6b4ad0 | ||
|
|
4ce1681e79 | ||
|
|
7570fa71f0 | ||
|
|
8a61c76cd9 | ||
|
|
fbf5381699 | ||
|
|
ff77f0db4f | ||
|
|
2e30abedc9 | ||
|
|
c4cb4f7475 | ||
|
|
7432bb813e | ||
|
|
572a104779 | ||
|
|
f77dc23d92 | ||
|
|
e92fc74dd3 | ||
|
|
9479c3d5c3 | ||
|
|
cfc37a64e1 | ||
|
|
2d5294804c | ||
|
|
6c01a85077 | ||
|
|
cf41e988eb | ||
|
|
d7a161c088 | ||
|
|
379c4040fe | ||
|
|
3f17618ad5 | ||
|
|
d7be64c3d9 | ||
|
|
ef7737e9be | ||
|
|
51ad3184b6 | ||
|
|
efc8fd878a | ||
|
|
6da1e98c83 | ||
|
|
513a06740c | ||
|
|
743a070724 | ||
|
|
5fb0e76669 | ||
|
|
bedeab686e | ||
|
|
9d9fa352ce | ||
|
|
1bfa6ce862 | ||
|
|
755b29c859 |
19
.vscode/launch.json
vendored
Normal file
19
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Next.js: debug full stack",
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/next",
|
||||||
|
"serverReadyAction": {
|
||||||
|
"pattern": "started server on .+, url: (https?://.+)",
|
||||||
|
"uriFormat": "%s",
|
||||||
|
"action": "debugWithChrome"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
10
README.md
10
README.md
@@ -9,7 +9,7 @@
|
|||||||
- Images built for AMD64 (x86_64), ARM64, ARMv7 and ARMv6
|
- Images built for AMD64 (x86_64), ARM64, ARMv7 and ARMv6
|
||||||
- Supports all Raspberry Pi's, most SBCs & Apple Silicon
|
- Supports all Raspberry Pi's, most SBCs & Apple Silicon
|
||||||
- Full i18n support with automatic language detection
|
- Full i18n support with automatic language detection
|
||||||
- Translations for Chinese, Dutch, French, German, Norwegian Bokmål, Polish, Portuguese, Russian and Spanish
|
- Translations for Chinese, Dutch, French, German, Norwegian Bokmål, Polish, Portuguese, Russian, Spanish and Swedish
|
||||||
- Want to help translate? [Join the Weblate project](https://hosted.weblate.org/engage/homepage/)
|
- Want to help translate? [Join the Weblate project](https://hosted.weblate.org/engage/homepage/)
|
||||||
- Service & Web Bookmarks
|
- Service & Web Bookmarks
|
||||||
- Docker Integration
|
- Docker Integration
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
- Service Integration
|
- Service Integration
|
||||||
- Sonarr, Radarr, Readarr, Prowlarr, Bazarr, Lidarr, Emby, Jellyfin, Tautulli (Plex)
|
- Sonarr, Radarr, Readarr, Prowlarr, Bazarr, Lidarr, Emby, Jellyfin, Tautulli (Plex)
|
||||||
- Ombi, Overseerr, Jellyseerr, Jackett, NZBGet, SABnzbd, ruTorrent, Transmission
|
- Ombi, Overseerr, Jellyseerr, Jackett, NZBGet, SABnzbd, ruTorrent, Transmission
|
||||||
- Portainer, Traefik, Speedtest Tracker, PiHole, Nginx Proxy Manager, Gotify
|
- Portainer, Traefik, Speedtest Tracker, PiHole, AdGuard Home, Nginx Proxy Manager, Gotify
|
||||||
- Information Providers
|
- Information Providers
|
||||||
- Coin Market Cap
|
- Coin Market Cap
|
||||||
- Information & Utility Widgets
|
- Information & Utility Widgets
|
||||||
@@ -50,11 +50,10 @@ services:
|
|||||||
homepage:
|
homepage:
|
||||||
image: ghcr.io/benphelps/homepage:latest
|
image: ghcr.io/benphelps/homepage:latest
|
||||||
container_name: homepage
|
container_name: homepage
|
||||||
user: 1000:1000 # Optional, change to your user and group IDs for permissions
|
|
||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 3000:3000
|
||||||
volumes:
|
volumes:
|
||||||
- /path/to/config:/app/config # Make sure your local config directory is exists
|
- /path/to/config:/app/config # Make sure your local config directory exists
|
||||||
- /var/run/docker.sock:/var/run/docker.sock # (optional) For docker integrations
|
- /var/run/docker.sock:/var/run/docker.sock # (optional) For docker integrations
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -129,10 +128,13 @@ Huge thanks to the all the contributors who have helped make this project what i
|
|||||||
- [ItsJustMeChris](https://github.com/benphelps/homepage/commits?author=ItsJustMeChris) - Coin Market Cap Widget
|
- [ItsJustMeChris](https://github.com/benphelps/homepage/commits?author=ItsJustMeChris) - Coin Market Cap Widget
|
||||||
- [jackblk](https://github.com/benphelps/homepage/commits?author=jackblk) - Vietnamese Translation
|
- [jackblk](https://github.com/benphelps/homepage/commits?author=jackblk) - Vietnamese Translation
|
||||||
- [JazzFisch](https://github.com/benphelps/homepage/commits?author=JazzFisch) - Readarr, Bazarr, Lidarr, SABnzbd & Transmission Integrations
|
- [JazzFisch](https://github.com/benphelps/homepage/commits?author=JazzFisch) - Readarr, Bazarr, Lidarr, SABnzbd & Transmission Integrations
|
||||||
|
- [juanmanuelbc](https://github.com/benphelps/homepage/commits?author=juanmanuelbc) - Spanish and Catalan Translations
|
||||||
- [modem7](https://github.com/benphelps/homepage/commits?author=modem7) - Impvoed Docker Image
|
- [modem7](https://github.com/benphelps/homepage/commits?author=modem7) - Impvoed Docker Image
|
||||||
- [nicedc](https://github.com/benphelps/homepage/commits?author=nicedc) - Chinese Translation
|
- [nicedc](https://github.com/benphelps/homepage/commits?author=nicedc) - Chinese Translation
|
||||||
- [Nonoss117](https://github.com/benphelps/homepage/commits?author=Nonoss117) - French Translation
|
- [Nonoss117](https://github.com/benphelps/homepage/commits?author=Nonoss117) - French Translation
|
||||||
|
- [pacoculebras](https://github.com/benphelps/homepage/commits?author=pacoculebras) - Catalan Translation
|
||||||
- [psychodracon](https://github.com/benphelps/homepage/commits?author=psychodracon) - Polish Translation
|
- [psychodracon](https://github.com/benphelps/homepage/commits?author=psychodracon) - Polish Translation
|
||||||
- [quod](https://github.com/benphelps/homepage/commits?author=quod) - Fixed Typos
|
- [quod](https://github.com/benphelps/homepage/commits?author=quod) - Fixed Typos
|
||||||
- [schklom](https://github.com/benphelps/homepage/commits?author=schklom) - ARM64, ARMv7 and ARMv6
|
- [schklom](https://github.com/benphelps/homepage/commits?author=schklom) - ARM64, ARMv7 and ARMv6
|
||||||
|
- [SuperDOS](https://github.com/benphelps/homepage/commits?author=SuperDOS) - Swedish Translation
|
||||||
- [xicopitz](https://github.com/benphelps/homepage/commits?author=xicopitz) - Gotify & Prowlarr Integration
|
- [xicopitz](https://github.com/benphelps/homepage/commits?author=xicopitz) - Gotify & Prowlarr Integration
|
||||||
|
|||||||
@@ -151,5 +151,11 @@
|
|||||||
"blocked": "Blocked",
|
"blocked": "Blocked",
|
||||||
"filtered": "Filtered",
|
"filtered": "Filtered",
|
||||||
"latency": "Latency"
|
"latency": "Latency"
|
||||||
|
},
|
||||||
|
"qbittorrent": {
|
||||||
|
"download": "Download",
|
||||||
|
"upload": "Upload",
|
||||||
|
"leech": "Leech",
|
||||||
|
"seed": "Seed"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,5 +151,11 @@
|
|||||||
"blocked": "Blocked",
|
"blocked": "Blocked",
|
||||||
"filtered": "Filtered",
|
"filtered": "Filtered",
|
||||||
"latency": "Latency"
|
"latency": "Latency"
|
||||||
|
},
|
||||||
|
"qbittorrent": {
|
||||||
|
"download": "Download",
|
||||||
|
"upload": "Upload",
|
||||||
|
"leech": "Leech",
|
||||||
|
"seed": "Seed"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,12 @@
|
|||||||
"leech": "Leech",
|
"leech": "Leech",
|
||||||
"seed": "Seed"
|
"seed": "Seed"
|
||||||
},
|
},
|
||||||
|
"qbittorrent": {
|
||||||
|
"download": "Download",
|
||||||
|
"upload": "Upload",
|
||||||
|
"leech": "Leech",
|
||||||
|
"seed": "Seed"
|
||||||
|
},
|
||||||
"sonarr": {
|
"sonarr": {
|
||||||
"wanted": "Wanted",
|
"wanted": "Wanted",
|
||||||
"queued": "Queued",
|
"queued": "Queued",
|
||||||
|
|||||||
@@ -147,9 +147,15 @@
|
|||||||
"albums": "Álbumes"
|
"albums": "Álbumes"
|
||||||
},
|
},
|
||||||
"adguard": {
|
"adguard": {
|
||||||
"queries": "Queries",
|
"queries": "Consultas",
|
||||||
"blocked": "Blocked",
|
"blocked": "Bloqueado",
|
||||||
"filtered": "Filtered",
|
"filtered": "Filtrado",
|
||||||
"latency": "Latency"
|
"latency": "Latencia"
|
||||||
|
},
|
||||||
|
"qbittorrent": {
|
||||||
|
"download": "Download",
|
||||||
|
"upload": "Upload",
|
||||||
|
"leech": "Leech",
|
||||||
|
"seed": "Seed"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,9 +127,9 @@
|
|||||||
"30days": "30 Jours"
|
"30days": "30 Jours"
|
||||||
},
|
},
|
||||||
"gotify": {
|
"gotify": {
|
||||||
"apps": "Applications",
|
"apps": "Applis",
|
||||||
"clients": "Clients",
|
"clients": "Clients",
|
||||||
"messages": "Messages"
|
"messages": "Msg"
|
||||||
},
|
},
|
||||||
"prowlarr": {
|
"prowlarr": {
|
||||||
"enableIndexers": "Indexeurs",
|
"enableIndexers": "Indexeurs",
|
||||||
@@ -158,9 +158,15 @@
|
|||||||
"albums": "Albums"
|
"albums": "Albums"
|
||||||
},
|
},
|
||||||
"adguard": {
|
"adguard": {
|
||||||
"queries": "Queries",
|
"queries": "Requêtes",
|
||||||
"blocked": "Blocked",
|
"blocked": "Bloquées",
|
||||||
"filtered": "Filtered",
|
"filtered": "Filtrées",
|
||||||
"latency": "Latency"
|
"latency": "Latence"
|
||||||
|
},
|
||||||
|
"qbittorrent": {
|
||||||
|
"download": "Download",
|
||||||
|
"upload": "Upload",
|
||||||
|
"leech": "Leech",
|
||||||
|
"seed": "Seed"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
161
public/locales/hr/common.json
Normal file
161
public/locales/hr/common.json
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
{
|
||||||
|
"weather": {
|
||||||
|
"current": "Current Location",
|
||||||
|
"allow": "Click to allow",
|
||||||
|
"updating": "Updating",
|
||||||
|
"wait": "Please wait"
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"placeholder": "Search…"
|
||||||
|
},
|
||||||
|
"resources": {
|
||||||
|
"total": "Total",
|
||||||
|
"free": "Free",
|
||||||
|
"used": "Used",
|
||||||
|
"load": "Load"
|
||||||
|
},
|
||||||
|
"sabnzbd": {
|
||||||
|
"rate": "Rate",
|
||||||
|
"queue": "Queue",
|
||||||
|
"timeleft": "Time Left"
|
||||||
|
},
|
||||||
|
"overseerr": {
|
||||||
|
"available": "Available",
|
||||||
|
"pending": "Pending",
|
||||||
|
"approved": "Approved"
|
||||||
|
},
|
||||||
|
"pihole": {
|
||||||
|
"queries": "Queries",
|
||||||
|
"blocked": "Blocked",
|
||||||
|
"gravity": "Gravity"
|
||||||
|
},
|
||||||
|
"adguard": {
|
||||||
|
"latency": "Latency",
|
||||||
|
"queries": "Queries",
|
||||||
|
"blocked": "Blocked",
|
||||||
|
"filtered": "Filtered"
|
||||||
|
},
|
||||||
|
"npm": {
|
||||||
|
"total": "Total",
|
||||||
|
"enabled": "Enabled",
|
||||||
|
"disabled": "Disabled"
|
||||||
|
},
|
||||||
|
"coinmarketcap": {
|
||||||
|
"configure": "Configure one or more crypto currencies to track",
|
||||||
|
"1hour": "1 Hour",
|
||||||
|
"1day": "1 Day",
|
||||||
|
"7days": "7 Days",
|
||||||
|
"30days": "30 Days"
|
||||||
|
},
|
||||||
|
"prowlarr": {
|
||||||
|
"enableIndexers": "Indexers",
|
||||||
|
"numberOfGrabs": "Grabs",
|
||||||
|
"numberOfQueries": "Queries",
|
||||||
|
"numberOfFailGrabs": "Fail Grabs",
|
||||||
|
"numberOfFailQueries": "Fail Queries"
|
||||||
|
},
|
||||||
|
"widget": {
|
||||||
|
"missing_type": "Missing Widget Type: {{type}}",
|
||||||
|
"api_error": "API Error",
|
||||||
|
"status": "Status"
|
||||||
|
},
|
||||||
|
"docker": {
|
||||||
|
"rx": "RX",
|
||||||
|
"tx": "TX",
|
||||||
|
"mem": "MEM",
|
||||||
|
"cpu": "CPU",
|
||||||
|
"offline": "Offline"
|
||||||
|
},
|
||||||
|
"emby": {
|
||||||
|
"playing": "Playing",
|
||||||
|
"transcoding": "Transcoding",
|
||||||
|
"bitrate": "Bitrate",
|
||||||
|
"no_active": "No Active Streams"
|
||||||
|
},
|
||||||
|
"tautulli": {
|
||||||
|
"playing": "Playing",
|
||||||
|
"transcoding": "Transcoding",
|
||||||
|
"bitrate": "Bitrate",
|
||||||
|
"no_active": "No Active Streams"
|
||||||
|
},
|
||||||
|
"nzbget": {
|
||||||
|
"rate": "Rate",
|
||||||
|
"remaining": "Remaining",
|
||||||
|
"downloaded": "Downloaded"
|
||||||
|
},
|
||||||
|
"rutorrent": {
|
||||||
|
"upload": "Upload",
|
||||||
|
"download": "Download",
|
||||||
|
"active": "Active"
|
||||||
|
},
|
||||||
|
"transmission": {
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"speedtest": {
|
||||||
|
"upload": "Upload",
|
||||||
|
"download": "Download",
|
||||||
|
"ping": "Ping"
|
||||||
|
},
|
||||||
|
"portainer": {
|
||||||
|
"running": "Running",
|
||||||
|
"stopped": "Stopped",
|
||||||
|
"total": "Total"
|
||||||
|
},
|
||||||
|
"traefik": {
|
||||||
|
"routers": "Routers",
|
||||||
|
"services": "Services",
|
||||||
|
"middleware": "Middleware"
|
||||||
|
},
|
||||||
|
"gotify": {
|
||||||
|
"clients": "Clients",
|
||||||
|
"messages": "Messages",
|
||||||
|
"apps": "Applications"
|
||||||
|
},
|
||||||
|
"jackett": {
|
||||||
|
"configured": "Configured",
|
||||||
|
"errored": "Errored"
|
||||||
|
},
|
||||||
|
"qbittorrent": {
|
||||||
|
"download": "Download",
|
||||||
|
"upload": "Upload",
|
||||||
|
"leech": "Leech",
|
||||||
|
"seed": "Seed"
|
||||||
|
}
|
||||||
|
}
|
||||||
161
public/locales/hu/common.json
Normal file
161
public/locales/hu/common.json
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
{
|
||||||
|
"resources": {
|
||||||
|
"total": "Total",
|
||||||
|
"free": "Free",
|
||||||
|
"used": "Used",
|
||||||
|
"load": "Load"
|
||||||
|
},
|
||||||
|
"docker": {
|
||||||
|
"rx": "RX",
|
||||||
|
"tx": "TX",
|
||||||
|
"mem": "MEM",
|
||||||
|
"cpu": "CPU",
|
||||||
|
"offline": "Offline"
|
||||||
|
},
|
||||||
|
"lidarr": {
|
||||||
|
"albums": "Albums",
|
||||||
|
"wanted": "Wanted",
|
||||||
|
"queued": "Queued"
|
||||||
|
},
|
||||||
|
"readarr": {
|
||||||
|
"wanted": "Wanted",
|
||||||
|
"queued": "Queued",
|
||||||
|
"books": "Books"
|
||||||
|
},
|
||||||
|
"bazarr": {
|
||||||
|
"missingEpisodes": "Missing Episodes",
|
||||||
|
"missingMovies": "Missing Movies"
|
||||||
|
},
|
||||||
|
"widget": {
|
||||||
|
"missing_type": "Missing Widget Type: {{type}}",
|
||||||
|
"api_error": "API Error",
|
||||||
|
"status": "Status"
|
||||||
|
},
|
||||||
|
"weather": {
|
||||||
|
"current": "Current Location",
|
||||||
|
"allow": "Click to allow",
|
||||||
|
"updating": "Updating",
|
||||||
|
"wait": "Please wait"
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"placeholder": "Search…"
|
||||||
|
},
|
||||||
|
"emby": {
|
||||||
|
"playing": "Playing",
|
||||||
|
"transcoding": "Transcoding",
|
||||||
|
"bitrate": "Bitrate",
|
||||||
|
"no_active": "No Active Streams"
|
||||||
|
},
|
||||||
|
"tautulli": {
|
||||||
|
"playing": "Playing",
|
||||||
|
"transcoding": "Transcoding",
|
||||||
|
"bitrate": "Bitrate",
|
||||||
|
"no_active": "No Active Streams"
|
||||||
|
},
|
||||||
|
"nzbget": {
|
||||||
|
"rate": "Rate",
|
||||||
|
"remaining": "Remaining",
|
||||||
|
"downloaded": "Downloaded"
|
||||||
|
},
|
||||||
|
"sabnzbd": {
|
||||||
|
"rate": "Rate",
|
||||||
|
"queue": "Queue",
|
||||||
|
"timeleft": "Time Left"
|
||||||
|
},
|
||||||
|
"rutorrent": {
|
||||||
|
"active": "Active",
|
||||||
|
"upload": "Upload",
|
||||||
|
"download": "Download"
|
||||||
|
},
|
||||||
|
"transmission": {
|
||||||
|
"leech": "Leech",
|
||||||
|
"seed": "Seed",
|
||||||
|
"download": "Download",
|
||||||
|
"upload": "Upload"
|
||||||
|
},
|
||||||
|
"qbittorrent": {
|
||||||
|
"download": "Download",
|
||||||
|
"upload": "Upload",
|
||||||
|
"leech": "Leech",
|
||||||
|
"seed": "Seed"
|
||||||
|
},
|
||||||
|
"sonarr": {
|
||||||
|
"wanted": "Wanted",
|
||||||
|
"queued": "Queued",
|
||||||
|
"series": "Series"
|
||||||
|
},
|
||||||
|
"radarr": {
|
||||||
|
"wanted": "Wanted",
|
||||||
|
"queued": "Queued",
|
||||||
|
"movies": "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",
|
||||||
|
"numberOfFailGrabs": "Fail Grabs",
|
||||||
|
"numberOfQueries": "Queries",
|
||||||
|
"numberOfFailQueries": "Fail Queries"
|
||||||
|
},
|
||||||
|
"jackett": {
|
||||||
|
"configured": "Configured",
|
||||||
|
"errored": "Errored"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -151,5 +151,11 @@
|
|||||||
"blocked": "Blocked",
|
"blocked": "Blocked",
|
||||||
"filtered": "Filtered",
|
"filtered": "Filtered",
|
||||||
"latency": "Latency"
|
"latency": "Latency"
|
||||||
|
},
|
||||||
|
"qbittorrent": {
|
||||||
|
"download": "Download",
|
||||||
|
"leech": "Leech",
|
||||||
|
"upload": "Upload",
|
||||||
|
"seed": "Seed"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,5 +151,11 @@
|
|||||||
"blocked": "Blocked",
|
"blocked": "Blocked",
|
||||||
"filtered": "Filtered",
|
"filtered": "Filtered",
|
||||||
"latency": "Latency"
|
"latency": "Latency"
|
||||||
|
},
|
||||||
|
"qbittorrent": {
|
||||||
|
"download": "Download",
|
||||||
|
"upload": "Upload",
|
||||||
|
"leech": "Leech",
|
||||||
|
"seed": "Seed"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,5 +151,11 @@
|
|||||||
"blocked": "Blocked",
|
"blocked": "Blocked",
|
||||||
"filtered": "Filtered",
|
"filtered": "Filtered",
|
||||||
"latency": "Latency"
|
"latency": "Latency"
|
||||||
|
},
|
||||||
|
"qbittorrent": {
|
||||||
|
"download": "Download",
|
||||||
|
"upload": "Upload",
|
||||||
|
"leech": "Leech",
|
||||||
|
"seed": "Seed"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,5 +151,11 @@
|
|||||||
"blocked": "Blocked",
|
"blocked": "Blocked",
|
||||||
"filtered": "Filtered",
|
"filtered": "Filtered",
|
||||||
"latency": "Latency"
|
"latency": "Latency"
|
||||||
|
},
|
||||||
|
"qbittorrent": {
|
||||||
|
"download": "Download",
|
||||||
|
"upload": "Upload",
|
||||||
|
"leech": "Leech",
|
||||||
|
"seed": "Seed"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,5 +162,11 @@
|
|||||||
"blocked": "Blocked",
|
"blocked": "Blocked",
|
||||||
"filtered": "Filtered",
|
"filtered": "Filtered",
|
||||||
"latency": "Latency"
|
"latency": "Latency"
|
||||||
|
},
|
||||||
|
"qbittorrent": {
|
||||||
|
"download": "Download",
|
||||||
|
"upload": "Upload",
|
||||||
|
"leech": "Leech",
|
||||||
|
"seed": "Seed"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,5 +151,11 @@
|
|||||||
"blocked": "Blocked",
|
"blocked": "Blocked",
|
||||||
"filtered": "Filtered",
|
"filtered": "Filtered",
|
||||||
"latency": "Latency"
|
"latency": "Latency"
|
||||||
|
},
|
||||||
|
"qbittorrent": {
|
||||||
|
"download": "Download",
|
||||||
|
"upload": "Upload",
|
||||||
|
"leech": "Leech",
|
||||||
|
"seed": "Seed"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,5 +151,11 @@
|
|||||||
"blocked": "Blocked",
|
"blocked": "Blocked",
|
||||||
"filtered": "Filtered",
|
"filtered": "Filtered",
|
||||||
"latency": "Latency"
|
"latency": "Latency"
|
||||||
|
},
|
||||||
|
"qbittorrent": {
|
||||||
|
"download": "Download",
|
||||||
|
"upload": "Upload",
|
||||||
|
"leech": "Leech",
|
||||||
|
"seed": "Seed"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,5 +151,11 @@
|
|||||||
"blocked": "Blocked",
|
"blocked": "Blocked",
|
||||||
"filtered": "Filtered",
|
"filtered": "Filtered",
|
||||||
"latency": "Latency"
|
"latency": "Latency"
|
||||||
|
},
|
||||||
|
"qbittorrent": {
|
||||||
|
"download": "Download",
|
||||||
|
"upload": "Upload",
|
||||||
|
"leech": "Leech",
|
||||||
|
"seed": "Seed"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"widget": {
|
"widget": {
|
||||||
"missing_type": "缺少小部件类型:{{type}}",
|
"missing_type": "缺少小部件类型:{{type}}",
|
||||||
"api_error": "API错误",
|
"api_error": "API错误",
|
||||||
"status": "地位"
|
"status": "状态"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "搜索…"
|
"placeholder": "搜索…"
|
||||||
@@ -88,7 +88,7 @@
|
|||||||
"total": "全部的"
|
"total": "全部的"
|
||||||
},
|
},
|
||||||
"weather": {
|
"weather": {
|
||||||
"current": "当前位置",
|
"current": "当前定位",
|
||||||
"allow": "点击并允许",
|
"allow": "点击并允许",
|
||||||
"updating": "更新中",
|
"updating": "更新中",
|
||||||
"wait": "请等待"
|
"wait": "请等待"
|
||||||
@@ -151,5 +151,11 @@
|
|||||||
"blocked": "Blocked",
|
"blocked": "Blocked",
|
||||||
"filtered": "Filtered",
|
"filtered": "Filtered",
|
||||||
"latency": "Latency"
|
"latency": "Latency"
|
||||||
|
},
|
||||||
|
"qbittorrent": {
|
||||||
|
"download": "Download",
|
||||||
|
"upload": "Upload",
|
||||||
|
"leech": "Leech",
|
||||||
|
"seed": "Seed"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,5 +151,11 @@
|
|||||||
"blocked": "Blocked",
|
"blocked": "Blocked",
|
||||||
"filtered": "Filtered",
|
"filtered": "Filtered",
|
||||||
"latency": "Latency"
|
"latency": "Latency"
|
||||||
|
},
|
||||||
|
"qbittorrent": {
|
||||||
|
"download": "Download",
|
||||||
|
"upload": "Upload",
|
||||||
|
"leech": "Leech",
|
||||||
|
"seed": "Seed"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import Emby from "./widgets/service/emby";
|
|||||||
import Nzbget from "./widgets/service/nzbget";
|
import Nzbget from "./widgets/service/nzbget";
|
||||||
import SABnzbd from "./widgets/service/sabnzbd";
|
import SABnzbd from "./widgets/service/sabnzbd";
|
||||||
import Transmission from "./widgets/service/transmission";
|
import Transmission from "./widgets/service/transmission";
|
||||||
|
import QBittorrent from "./widgets/service/qbittorrent";
|
||||||
import Docker from "./widgets/service/docker";
|
import Docker from "./widgets/service/docker";
|
||||||
import Pihole from "./widgets/service/pihole";
|
import Pihole from "./widgets/service/pihole";
|
||||||
import Rutorrent from "./widgets/service/rutorrent";
|
import Rutorrent from "./widgets/service/rutorrent";
|
||||||
@@ -41,6 +42,7 @@ const widgetMappings = {
|
|||||||
nzbget: Nzbget,
|
nzbget: Nzbget,
|
||||||
sabnzbd: SABnzbd,
|
sabnzbd: SABnzbd,
|
||||||
transmission: Transmission,
|
transmission: Transmission,
|
||||||
|
qbittorrent: QBittorrent,
|
||||||
pihole: Pihole,
|
pihole: Pihole,
|
||||||
rutorrent: Rutorrent,
|
rutorrent: Rutorrent,
|
||||||
speedtest: Speedtest,
|
speedtest: Speedtest,
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ export default function Bazarr({ service }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<Block label={t("bazarr.missingEpisodes")} value={episodesData.total} />
|
<Block label={t("bazarr.missingEpisodes")} value={t("common.number", { value: episodesData.total })} />
|
||||||
<Block label={t("bazarr.missingMovies")} value={moviesData.total} />
|
<Block label={t("bazarr.missingMovies")} value={t("common.number", { value: moviesData.total })} />
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ export default function Jackett({ service }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<Block label={t("jackett.configured")} value={indexersData.length} />
|
<Block label={t("jackett.configured")} value={t("common.number", { value: indexersData.length })} />
|
||||||
<Block label={t("jackett.errored")} value={errored.length} />
|
<Block label={t("jackett.errored")} value={t("common.number", { value: errored.length })} />
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,13 +29,11 @@ export default function Lidarr({ service }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const have = albumsData.filter((album) => album.statistics.percentOfTracks === 100);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<Block label={t("lidarr.wanted")} value={wantedData.totalRecords} />
|
<Block label={t("lidarr.wanted")} value={t("common.number", { value: wantedData.totalRecords })} />
|
||||||
<Block label={t("lidarr.queued")} value={queueData.totalCount} />
|
<Block label={t("lidarr.queued")} value={t("common.number", { value: queueData.totalCount })} />
|
||||||
<Block label={t("lidarr.albums")} value={have.length} />
|
<Block label={t("lidarr.albums")} value={t("common.number", { value: albumsData.have })} />
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
69
src/components/services/widgets/service/qbittorrent.jsx
Normal file
69
src/components/services/widgets/service/qbittorrent.jsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
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 QBittorrent ({ service }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const config = service.widget;
|
||||||
|
|
||||||
|
const { data: torrentData, error: torrentError } = useSWR(formatApiUrl(config, "torrents/info"));
|
||||||
|
|
||||||
|
if (torrentError) {
|
||||||
|
return <Widget error={t("widget.api_error")} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!torrentData) {
|
||||||
|
return (
|
||||||
|
<Widget>
|
||||||
|
<Block label={t("qbittorrent.leech")} />
|
||||||
|
<Block label={t("qbittorrent.download")} />
|
||||||
|
<Block label={t("qbittorrent.seed")} />
|
||||||
|
<Block label={t("qbittorrent.upload")} />
|
||||||
|
</Widget>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let rateDl = 0;
|
||||||
|
let rateUl = 0;
|
||||||
|
let completed = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < torrentData.length; i += 1) {
|
||||||
|
const torrent = torrentData[i];
|
||||||
|
rateDl += torrent.dlspeed;
|
||||||
|
rateUl += torrent.upspeed;
|
||||||
|
if (torrent.progress === 1) {
|
||||||
|
completed += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const leech = torrentData.length - completed;
|
||||||
|
|
||||||
|
let unitsDl = "KB/s";
|
||||||
|
let unitsUl = "KB/s";
|
||||||
|
rateDl /= 1024;
|
||||||
|
rateUl /= 1024;
|
||||||
|
|
||||||
|
if (rateDl > 1024) {
|
||||||
|
rateDl /= 1024;
|
||||||
|
unitsDl = "MB/s";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rateUl > 1024) {
|
||||||
|
rateUl /= 1024;
|
||||||
|
unitsUl = "MB/s";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Widget>
|
||||||
|
<Block label={t("qbittorrent.leech")} value={t("common.number", { value: leech })} />
|
||||||
|
<Block label={t("qbittorrent.download")} value={`${rateDl.toFixed(2)} ${unitsDl}`} />
|
||||||
|
<Block label={t("qbittorrent.seed")} value={t("common.number", { value: completed })} />
|
||||||
|
<Block label={t("qbittorrent.upload")} value={`${rateUl.toFixed(2)} ${unitsUl}`} />
|
||||||
|
</Widget>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -28,14 +28,11 @@ export default function Radarr({ service }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const wanted = moviesData.filter((movie) => movie.isAvailable === false);
|
|
||||||
const have = moviesData.filter((movie) => movie.isAvailable === true);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<Block label={t("radarr.wanted")} value={wanted.length} />
|
<Block label={t("radarr.wanted")} value={moviesData.wanted} />
|
||||||
<Block label={t("radarr.queued")} value={queuedData.totalCount} />
|
<Block label={t("radarr.queued")} value={queuedData.totalCount} />
|
||||||
<Block label={t("radarr.movies")} value={have.length} />
|
<Block label={t("radarr.movies")} value={moviesData.have} />
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,13 +29,11 @@ export default function Readarr({ service }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const have = booksData.filter((book) => book.statistics.bookFileCount > 0);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<Block label={t("readarr.wanted")} value={wantedData.totalRecords} />
|
<Block label={t("readarr.wanted")} value={t("common.number", { value: wantedData.totalRecords })} />
|
||||||
<Block label={t("readarr.queued")} value={queueData.totalCount} />
|
<Block label={t("readarr.queued")} value={t("common.number", { value: queueData.totalCount })} />
|
||||||
<Block label={t("readarr.books")} value={have.length} />
|
<Block label={t("readarr.books")} value={t("common.number", { value: booksData.have })} />
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export default function SABnzbd({ service }) {
|
|||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<Block label={t("sabnzbd.rate")} value={`${queueData.queue.speed}B/s`} />
|
<Block label={t("sabnzbd.rate")} value={`${queueData.queue.speed}B/s`} />
|
||||||
<Block label={t("sabnzbd.queue")} value={queueData.queue.noofslots} />
|
<Block label={t("sabnzbd.queue")} value={t("common.number", { value: queueData.queue.noofslots })} />
|
||||||
<Block label={t("sabnzbd.timeleft")} value={queueData.queue.timeleft} />
|
<Block label={t("sabnzbd.timeleft")} value={queueData.queue.timeleft} />
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export default function Sonarr({ service }) {
|
|||||||
<Widget>
|
<Widget>
|
||||||
<Block label={t("sonarr.wanted")} value={wantedData.totalRecords} />
|
<Block label={t("sonarr.wanted")} value={wantedData.totalRecords} />
|
||||||
<Block label={t("sonarr.queued")} value={queuedData.totalRecords} />
|
<Block label={t("sonarr.queued")} value={queuedData.totalRecords} />
|
||||||
<Block label={t("sonarr.series")} value={seriesData.length} />
|
<Block label={t("sonarr.series")} value={seriesData.total} />
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,9 +61,9 @@ export default function Transmission({ service }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<Block label={t("transmission.leech")} value={leech} />
|
<Block label={t("transmission.leech")} value={t("common.number", { value: leech })} />
|
||||||
<Block label={t("transmission.download")} value={`${rateDl.toFixed(2)} ${unitsDl}`} />
|
<Block label={t("transmission.download")} value={`${rateDl.toFixed(2)} ${unitsDl}`} />
|
||||||
<Block label={t("transmission.seed")} value={completed} />
|
<Block label={t("transmission.seed")} value={t("common.number", { value: completed })} />
|
||||||
<Block label={t("transmission.upload")} value={`${rateUl.toFixed(2)} ${unitsUl}`} />
|
<Block label={t("transmission.upload")} value={`${rateUl.toFixed(2)} ${unitsUl}`} />
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,15 +7,17 @@ export default async function handler(req, res) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
discoveredServices = cleanServiceGroups(await servicesFromDocker());
|
discoveredServices = cleanServiceGroups(await servicesFromDocker());
|
||||||
} catch {
|
} catch (e) {
|
||||||
console.error("Failed to discover services, please check docker.yaml for errors");
|
console.error("Failed to discover services, please check docker.yaml for errors or remove example entries.");
|
||||||
|
console.error(e);
|
||||||
discoveredServices = [];
|
discoveredServices = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
configuredServices = cleanServiceGroups(await servicesFromConfig());
|
configuredServices = cleanServiceGroups(await servicesFromConfig());
|
||||||
} catch {
|
} catch (e) {
|
||||||
console.error("Failed to load services.yaml, please check for errors");
|
console.error("Failed to load services.yaml, please check for errors");
|
||||||
|
console.error(e);
|
||||||
configuredServices = [];
|
configuredServices = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,17 +4,77 @@ import rutorrentProxyHandler from "utils/proxies/rutorrent";
|
|||||||
import nzbgetProxyHandler from "utils/proxies/nzbget";
|
import nzbgetProxyHandler from "utils/proxies/nzbget";
|
||||||
import npmProxyHandler from "utils/proxies/npm";
|
import npmProxyHandler from "utils/proxies/npm";
|
||||||
import transmissionProxyHandler from "utils/proxies/transmission";
|
import transmissionProxyHandler from "utils/proxies/transmission";
|
||||||
|
import qbittorrentProxyHandler from "utils/proxies/qbittorrent";
|
||||||
|
|
||||||
|
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 = {
|
const serviceProxyHandlers = {
|
||||||
// uses query param auth
|
// uses query param auth
|
||||||
emby: genericProxyHandler,
|
emby: genericProxyHandler,
|
||||||
jellyfin: genericProxyHandler,
|
jellyfin: genericProxyHandler,
|
||||||
pihole: genericProxyHandler,
|
pihole: genericProxyHandler,
|
||||||
radarr: genericProxyHandler,
|
radarr: {
|
||||||
sonarr: genericProxyHandler,
|
proxy: genericProxyHandler,
|
||||||
lidarr: genericProxyHandler,
|
maps: {
|
||||||
readarr: genericProxyHandler,
|
movie: (data) => ({
|
||||||
bazarr: genericProxyHandler,
|
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,
|
speedtest: genericProxyHandler,
|
||||||
tautulli: genericProxyHandler,
|
tautulli: genericProxyHandler,
|
||||||
traefik: genericProxyHandler,
|
traefik: genericProxyHandler,
|
||||||
@@ -34,6 +94,7 @@ const serviceProxyHandlers = {
|
|||||||
nzbget: nzbgetProxyHandler,
|
nzbget: nzbgetProxyHandler,
|
||||||
npm: npmProxyHandler,
|
npm: npmProxyHandler,
|
||||||
transmission: transmissionProxyHandler,
|
transmission: transmissionProxyHandler,
|
||||||
|
qbittorrent: qbittorrentProxyHandler,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
@@ -42,8 +103,15 @@ export default async function handler(req, res) {
|
|||||||
const serviceProxyHandler = serviceProxyHandlers[type];
|
const serviceProxyHandler = serviceProxyHandlers[type];
|
||||||
|
|
||||||
if (serviceProxyHandler) {
|
if (serviceProxyHandler) {
|
||||||
|
if (serviceProxyHandler instanceof Function) {
|
||||||
return serviceProxyHandler(req, res);
|
return serviceProxyHandler(req, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { proxy, maps } = serviceProxyHandler;
|
||||||
|
if (proxy) {
|
||||||
|
return proxy(req, res, maps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return res.status(403).json({ error: "Unkown proxy service type" });
|
return res.status(403).json({ error: "Unkown proxy service type" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export default function Home({ settings }) {
|
|||||||
if (settings.background) {
|
if (settings.background) {
|
||||||
wrappedStyle.backgroundImage = `url(${settings.background})`;
|
wrappedStyle.backgroundImage = `url(${settings.background})`;
|
||||||
wrappedStyle.backgroundSize = "cover";
|
wrappedStyle.backgroundSize = "cover";
|
||||||
|
wrappedStyle.opacity = settings.backgroundOpacity ?? 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const formats = {
|
|||||||
portainer: `{url}/api/endpoints/{env}/{endpoint}`,
|
portainer: `{url}/api/endpoints/{env}/{endpoint}`,
|
||||||
rutorrent: `{url}/plugins/httprpc/action.php`,
|
rutorrent: `{url}/plugins/httprpc/action.php`,
|
||||||
transmission: `{url}/transmission/rpc`,
|
transmission: `{url}/transmission/rpc`,
|
||||||
|
qbittorrent: `{url}/api/v2/{endpoint}`,
|
||||||
jellyseerr: `{url}/api/v1/{endpoint}`,
|
jellyseerr: `{url}/api/v1/{endpoint}`,
|
||||||
overseerr: `{url}/api/v1/{endpoint}`,
|
overseerr: `{url}/api/v1/{endpoint}`,
|
||||||
ombi: `{url}/api/v1/{endpoint}`,
|
ombi: `{url}/api/v1/{endpoint}`,
|
||||||
|
|||||||
34
src/utils/cookie-jar.js
Normal file
34
src/utils/cookie-jar.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/* eslint-disable no-param-reassign */
|
||||||
|
import { Cookie, CookieJar } from 'tough-cookie';
|
||||||
|
|
||||||
|
const cookieJar = new CookieJar();
|
||||||
|
|
||||||
|
export function setCookieHeader(url, params) {
|
||||||
|
// add cookie header, if we have one in the jar
|
||||||
|
const existingCookie = cookieJar.getCookieStringSync(url.toString());
|
||||||
|
if (existingCookie) {
|
||||||
|
params.headers = params.headers ?? {};
|
||||||
|
params.headers.Cookie = existingCookie;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addCookieToJar(url, headers) {
|
||||||
|
let cookieHeader = headers['set-cookie'];
|
||||||
|
if (headers instanceof Headers) {
|
||||||
|
cookieHeader = headers.get('set-cookie');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cookieHeader || cookieHeader.length === 0) return;
|
||||||
|
|
||||||
|
let cookies = null;
|
||||||
|
if (cookieHeader instanceof Array) {
|
||||||
|
cookies = cookieHeader.map(Cookie.parse);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
cookies = [Cookie.parse(cookieHeader)];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < cookies.length; i += 1) {
|
||||||
|
cookieJar.setCookieSync(cookies[i], url.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,39 +1,15 @@
|
|||||||
/* eslint-disable prefer-promise-reject-errors */
|
/* eslint-disable prefer-promise-reject-errors */
|
||||||
/* eslint-disable no-param-reassign */
|
/* eslint-disable no-param-reassign */
|
||||||
import { http, https } from "follow-redirects";
|
import { http, https } from "follow-redirects";
|
||||||
import { Cookie, CookieJar } from 'tough-cookie';
|
|
||||||
|
|
||||||
const cookieJar = new CookieJar();
|
import { addCookieToJar, setCookieHeader } from "utils/cookie-jar";
|
||||||
|
|
||||||
function setCookieHeader(url, params) {
|
|
||||||
// add cookie header, if we have one in the jar
|
|
||||||
const existingCookie = cookieJar.getCookieStringSync(url.toString());
|
|
||||||
if (existingCookie) {
|
|
||||||
params.headers = params.headers ?? {};
|
|
||||||
params.headers.Cookie = existingCookie;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addCookieHandler(url, params) {
|
function addCookieHandler(url, params) {
|
||||||
setCookieHeader(url, params);
|
setCookieHeader(url, params);
|
||||||
|
|
||||||
// handle cookies during redirects
|
// handle cookies during redirects
|
||||||
params.beforeRedirect = (options, responseInfo) => {
|
params.beforeRedirect = (options, responseInfo) => {
|
||||||
const cookieHeader = responseInfo.headers['set-cookie'];
|
addCookieToJar(options.href, responseInfo.headers);
|
||||||
if (!cookieHeader || cookieHeader.length === 0) return;
|
|
||||||
|
|
||||||
let cookies = null;
|
|
||||||
if (cookieHeader instanceof Array) {
|
|
||||||
cookies = cookieHeader.map(Cookie.parse);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
cookies = [Cookie.parse(cookieHeader)];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < cookies.length; i += 1) {
|
|
||||||
cookieJar.setCookieSync(cookies[i], options.href);
|
|
||||||
}
|
|
||||||
|
|
||||||
setCookieHeader(options.href, options);
|
setCookieHeader(options.href, options);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -49,6 +25,7 @@ export function httpsRequest(url, params) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
response.on("end", () => {
|
response.on("end", () => {
|
||||||
|
addCookieToJar(url, response.headers);
|
||||||
resolve([response.statusCode, response.headers["content-type"], Buffer.concat(data), response.headers]);
|
resolve([response.statusCode, response.headers["content-type"], Buffer.concat(data), response.headers]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -76,6 +53,7 @@ export function httpRequest(url, params) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
response.on("end", () => {
|
response.on("end", () => {
|
||||||
|
addCookieToJar(url, response.headers);
|
||||||
resolve([response.statusCode, response.headers["content-type"], Buffer.concat(data), response.headers]);
|
resolve([response.statusCode, response.headers["content-type"], Buffer.concat(data), response.headers]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import getServiceWidget from "utils/service-helpers";
|
|||||||
import { formatApiCall } from "utils/api-helpers";
|
import { formatApiCall } from "utils/api-helpers";
|
||||||
import { httpProxy } from "utils/http";
|
import { httpProxy } from "utils/http";
|
||||||
|
|
||||||
export default async function genericProxyHandler(req, res) {
|
export default async function genericProxyHandler(req, res, maps) {
|
||||||
const { group, service, endpoint } = req.query;
|
const { group, service, endpoint } = req.query;
|
||||||
|
|
||||||
if (group && service) {
|
if (group && service) {
|
||||||
@@ -23,13 +23,18 @@ export default async function genericProxyHandler(req, res) {
|
|||||||
headers,
|
headers,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let resultData = data;
|
||||||
|
if (maps?.[endpoint]) {
|
||||||
|
resultData = maps[endpoint](data);
|
||||||
|
}
|
||||||
|
|
||||||
if (contentType) res.setHeader("Content-Type", contentType);
|
if (contentType) res.setHeader("Content-Type", contentType);
|
||||||
|
|
||||||
if (status === 204 || status === 304) {
|
if (status === 204 || status === 304) {
|
||||||
return res.status(status).end();
|
return res.status(status).end();
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(status).send(data);
|
return res.status(status).send(resultData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
58
src/utils/proxies/qbittorrent.js
Normal file
58
src/utils/proxies/qbittorrent.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { formatApiCall } from "utils/api-helpers";
|
||||||
|
import { addCookieToJar, setCookieHeader } from "utils/cookie-jar";
|
||||||
|
import { httpProxy } from "utils/http";
|
||||||
|
import getServiceWidget from "utils/service-helpers";
|
||||||
|
|
||||||
|
async function login(widget, params) {
|
||||||
|
const loginUrl = new URL(`${widget.url}/api/v2/auth/login`);
|
||||||
|
const loginBody = `username=${encodeURI(widget.username)}&password=${encodeURI(widget.password)}`;
|
||||||
|
|
||||||
|
// using fetch intentionally, for login only, as the httpProxy method causes qBittorrent to
|
||||||
|
// complain about header encoding
|
||||||
|
return fetch(loginUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||||
|
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]));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function qbittorrentProxyHandler(req, res) {
|
||||||
|
const { group, service, endpoint } = req.query;
|
||||||
|
|
||||||
|
if (!group || !service) {
|
||||||
|
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const widget = await getServiceWidget(group, service);
|
||||||
|
|
||||||
|
if (!widget) {
|
||||||
|
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(formatApiCall(widget.type, { endpoint, ...widget }));
|
||||||
|
const params = { method: "GET", headers: {} };
|
||||||
|
setCookieHeader(url, params);
|
||||||
|
|
||||||
|
if (!params.headers.Cookie) {
|
||||||
|
const [status, data] = await login(widget, params);
|
||||||
|
|
||||||
|
if (status !== 200) {
|
||||||
|
return res.status(status).end(data);
|
||||||
|
}
|
||||||
|
if (data.toString() !== 'Ok.') {
|
||||||
|
return res.status(401).end(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [status, contentType, data] = await httpProxy(url, params);
|
||||||
|
|
||||||
|
if (contentType) res.setHeader("Content-Type", contentType);
|
||||||
|
return res.status(status).send(data);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user