Compare commits

..

104 Commits

Author SHA1 Message Date
Ben Phelps
330575bab3 fix links 2022-09-18 18:49:50 +03:00
Ben Phelps
30ec4aed28 Translated using Weblate (Dutch)
Currently translated at 58.3% (63 of 108 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/nl/
2022-09-18 16:53:21 +02:00
Ben Phelps
ee456fd8e5 Translated using Weblate (Italian)
Currently translated at 71.2% (77 of 108 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/it/
2022-09-18 16:53:21 +02:00
Ben Phelps
ed25c8a84b Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (108 of 108 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/zh_Hans/
2022-09-18 16:53:21 +02:00
Ben Phelps
b0a45fe09c Translated using Weblate (Russian)
Currently translated at 21.2% (23 of 108 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ru/
2022-09-18 16:53:20 +02:00
Nonoss117
50f0f46ad9 Translated using Weblate (French)
Currently translated at 100.0% (108 of 108 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/fr/
2022-09-18 16:53:20 +02:00
Ben Phelps
bd61d459ad Translated using Weblate (Spanish)
Currently translated at 100.0% (108 of 108 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/es/
2022-09-18 16:53:20 +02:00
Ben Phelps
4f73c60d37 Translated using Weblate (German)
Currently translated at 62.0% (67 of 108 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/de/
2022-09-18 16:53:20 +02:00
Ben Phelps
7c536f0cb0 update attribution sorting 2022-09-18 17:05:31 +03:00
Ben Phelps
d5a489198a update readme 2022-09-18 17:00:40 +03:00
Ben Phelps
17f54da524 yaml validation 2022-09-18 16:41:01 +03:00
ShlomiPorush
b5065673ab Translated using Weblate (Hebrew)
Currently translated at 93.5% (101 of 108 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/he/
2022-09-18 15:13:10 +02:00
Pacux
610b0f63e0 Translated using Weblate (Catalan)
Currently translated at 100.0% (108 of 108 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ca/
2022-09-18 15:13:09 +02:00
yahoo~~
73317bda67 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (108 of 108 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/zh_Hans/
2022-09-18 15:13:09 +02:00
Nonoss117
f690f3acba Translated using Weblate (French)
Currently translated at 100.0% (108 of 108 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/fr/
2022-09-18 15:13:09 +02:00
Anonymous
eea9f1f6cb Translated using Weblate (Hebrew)
Currently translated at 100.0% (0 of 0 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/he/
2022-09-18 08:37:27 +02:00
Daniel Varga
d9089e8d1c Translated using Weblate (Hungarian)
Currently translated at 100.0% (108 of 108 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/hu/
2022-09-18 08:37:26 +02:00
Nonoss117
bed5acc9d5 Translated using Weblate (French)
Currently translated at 100.0% (108 of 108 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/fr/
2022-09-18 08:37:25 +02:00
Ángel Fernández Sánchez
f46feff445 Translated using Weblate (Spanish)
Currently translated at 100.0% (108 of 108 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/es/
2022-09-18 08:37:25 +02:00
Daniel Varga
d46a98c7d5 Translated using Weblate (German)
Currently translated at 62.0% (67 of 108 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/de/
2022-09-18 08:37:25 +02:00
ShlomiPorush
05af60df4f Added translation using Weblate (Hebrew) 2022-09-18 08:37:17 +02:00
Ben Phelps
5fc266ed81 better status icon logic for tautulli 2022-09-18 02:12:24 +03:00
Ben Phelps
38356c31b0 handle when structure differs from expectation 2022-09-18 01:38:41 +03:00
Ben Phelps
2703cfb81e update attributions and features 2022-09-17 22:03:21 +03:00
Ben Phelps
8a226ca473 Merge pull request #198 from JazzFisch/fix-incorrect-widget-values
Fix issues with incorrect values in widgets
2022-09-17 19:21:26 +03:00
Jason Fischer
33e6d54fd2 Fix issues with incorrect values in widgets
associated: #180
associated: #194
2022-09-17 09:17:03 -07:00
Ben Phelps
d36f37a4ed remove as it’s causing troubles 2022-09-17 17:23:45 +03:00
Ben Phelps
f3ebbb6547 pass errors 2022-09-17 16:55:18 +03:00
Ben Phelps
28b2f79e5b use aggregate mapped data
to reduce the size of the API responses
2022-09-17 13:05:44 +03:00
Anonymous
9a77115a30 Translated using Weblate (Hungarian)
Currently translated at 100.0% (0 of 0 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/hu/
2022-09-17 09:25:24 +02:00
Daniel Varga
2d899e364d Added translation using Weblate (Hungarian) 2022-09-17 09:25:17 +02:00
Anonymous
32b881891c Translated using Weblate (Croatian)
Currently translated at 8.3% (9 of 108 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/hr/
2022-09-17 09:06:55 +02:00
Anonymous
9eefc07c7c Translated using Weblate (Swedish)
Currently translated at 88.8% (96 of 108 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/sv/
2022-09-17 09:06:55 +02:00
Anonymous
792accffb6 Translated using Weblate (Polish)
Currently translated at 82.4% (89 of 108 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/pl/
2022-09-17 09:06:54 +02:00
Anonymous
03af88aba5 Translated using Weblate (Catalan)
Currently translated at 92.5% (100 of 108 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ca/
2022-09-17 09:06:54 +02:00
Anonymous
f56b6b4ad0 Translated using Weblate (Chinese (Traditional))
Currently translated at 8.3% (9 of 108 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/zh_Hant/
2022-09-17 09:06:54 +02:00
Anonymous
4ce1681e79 Translated using Weblate (Dutch)
Currently translated at 57.4% (62 of 108 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/nl/
2022-09-17 09:06:54 +02:00
Anonymous
7570fa71f0 Translated using Weblate (Vietnamese)
Currently translated at 40.7% (44 of 108 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/vi/
2022-09-17 09:06:53 +02:00
Anonymous
8a61c76cd9 Translated using Weblate (Norwegian Bokmål)
Currently translated at 73.1% (79 of 108 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/nb_NO/
2022-09-17 09:06:53 +02:00
Anonymous
fbf5381699 Translated using Weblate (Italian)
Currently translated at 63.8% (69 of 108 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/it/
2022-09-17 09:06:53 +02:00
Anonymous
ff77f0db4f Translated using Weblate (Chinese (Simplified))
Currently translated at 76.8% (83 of 108 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/zh_Hans/
2022-09-17 09:06:52 +02:00
Anonymous
2e30abedc9 Translated using Weblate (Russian)
Currently translated at 19.4% (21 of 108 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ru/
2022-09-17 09:06:52 +02:00
Anonymous
c4cb4f7475 Translated using Weblate (Portuguese)
Currently translated at 81.4% (88 of 108 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/pt/
2022-09-17 09:06:52 +02:00
Anonymous
7432bb813e Translated using Weblate (French)
Currently translated at 96.2% (104 of 108 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/fr/
2022-09-17 09:06:52 +02:00
Anonymous
572a104779 Translated using Weblate (Spanish)
Currently translated at 96.2% (104 of 108 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/es/
2022-09-17 09:06:51 +02:00
Anonymous
f77dc23d92 Translated using Weblate (German)
Currently translated at 59.2% (64 of 108 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/de/
2022-09-17 09:06:51 +02:00
nicedc
e92fc74dd3 Translated using Weblate (Chinese (Simplified))
Currently translated at 79.8% (83 of 104 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/zh_Hans/
2022-09-17 09:06:42 +02:00
Nonoss117
9479c3d5c3 Translated using Weblate (French)
Currently translated at 100.0% (104 of 104 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/fr/
2022-09-17 09:06:42 +02:00
Ángel Fernández Sánchez
cfc37a64e1 Translated using Weblate (Spanish)
Currently translated at 100.0% (104 of 104 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/es/
2022-09-17 09:06:42 +02:00
Ben Phelps
2d5294804c Merge pull request #192 from JazzFisch/add-qbittorrent
Add qBittorrent Widget
2022-09-17 10:06:38 +03:00
Jason Fischer
6c01a85077 Merge branch 'main' into add-qbittorrent 2022-09-16 23:19:24 -07:00
Ben Phelps
cf41e988eb fix error with no map 2022-09-17 08:38:53 +03:00
Ben Phelps
d7a161c088 remove map for now 2022-09-17 08:34:32 +03:00
Ben Phelps
379c4040fe Merge branch 'JazzFisch-proxy-with-mapping' 2022-09-17 08:32:48 +03:00
Ben Phelps
3f17618ad5 allow endpoint specific maps 2022-09-17 08:32:40 +03:00
Andy
d7be64c3d9 add backgroundOpacity option 2022-09-17 08:24:12 +03:00
Juan Manuel Bennàssar Carretero
ef7737e9be Translated using Weblate (Spanish)
Currently translated at 100.0% (104 of 104 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/es/
2022-09-17 08:24:12 +03:00
Ben Phelps
51ad3184b6 Merge pull request #183 from andrii-kryvoviaz/add-background-image-opacity
Add backgroundOpacity option
2022-09-17 07:33:35 +03:00
Jason Fischer
efc8fd878a Merge branch 'main' into add-qbittorrent 2022-09-16 19:12:41 -07:00
Jason Fischer
6da1e98c83 Add qBittorrent Widget
- extract cookie jar functionality into its own file
- use i18n for more strings in existing widgets

completes: #152
associated: #123
2022-09-16 19:11:57 -07:00
Juan Manuel Bennàssar Carretero
513a06740c Translated using Weblate (Spanish)
Currently translated at 100.0% (104 of 104 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/es/
2022-09-16 21:23:35 +02:00
Jason Fischer
743a070724 Proposal to add ability to map data in a proxy 2022-09-16 11:33:11 -07:00
Andy
5fb0e76669 add backgroundOpacity option 2022-09-16 15:31:13 +03:00
Anonymous
bedeab686e Translated using Weblate (Croatian)
Currently translated at 100.0% (0 of 0 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/hr/
2022-09-16 14:31:03 +02:00
Nonoss117
9d9fa352ce Translated using Weblate (French)
Currently translated at 100.0% (104 of 104 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/fr/
2022-09-16 14:31:02 +02:00
sheep
1bfa6ce862 Added translation using Weblate (Croatian) 2022-09-16 14:30:39 +02:00
Ben Phelps
755b29c859 update readme 2022-09-16 14:18:27 +03:00
Anonymous
aab5b0247a Translated using Weblate (Swedish)
Currently translated at 92.3% (96 of 104 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/sv/
2022-09-16 13:06:38 +02:00
Anonymous
d7e4b0bd17 Translated using Weblate (Polish)
Currently translated at 85.5% (89 of 104 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/pl/
2022-09-16 13:06:38 +02:00
Anonymous
3bacdadb80 Translated using Weblate (Catalan)
Currently translated at 96.1% (100 of 104 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ca/
2022-09-16 13:06:38 +02:00
Anonymous
1d75ee44ed Translated using Weblate (Chinese (Traditional))
Currently translated at 8.6% (9 of 104 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/zh_Hant/
2022-09-16 13:06:37 +02:00
Anonymous
230cc343af Translated using Weblate (Dutch)
Currently translated at 59.6% (62 of 104 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/nl/
2022-09-16 13:06:37 +02:00
Anonymous
b318ee165c Translated using Weblate (Vietnamese)
Currently translated at 42.3% (44 of 104 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/vi/
2022-09-16 13:06:37 +02:00
Anonymous
f677646365 Translated using Weblate (Norwegian Bokmål)
Currently translated at 75.9% (79 of 104 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/nb_NO/
2022-09-16 13:06:36 +02:00
Anonymous
8db7d820d7 Translated using Weblate (Italian)
Currently translated at 66.3% (69 of 104 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/it/
2022-09-16 13:06:36 +02:00
Anonymous
adb0632566 Translated using Weblate (Chinese (Simplified))
Currently translated at 79.8% (83 of 104 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/zh_Hans/
2022-09-16 13:06:36 +02:00
Anonymous
4d5c8db333 Translated using Weblate (Russian)
Currently translated at 20.1% (21 of 104 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ru/
2022-09-16 13:06:35 +02:00
Anonymous
01d6a3d5f8 Translated using Weblate (Portuguese)
Currently translated at 84.6% (88 of 104 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/pt/
2022-09-16 13:06:35 +02:00
Anonymous
fbeadbc32f Translated using Weblate (French)
Currently translated at 96.1% (100 of 104 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/fr/
2022-09-16 13:06:35 +02:00
Anonymous
1289be888f Translated using Weblate (Spanish)
Currently translated at 96.1% (100 of 104 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/es/
2022-09-16 13:06:35 +02:00
Anonymous
aa7e3a955c Translated using Weblate (German)
Currently translated at 61.5% (64 of 104 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/de/
2022-09-16 13:06:34 +02:00
SuperDOS
2823f3b921 Translated using Weblate (Swedish)
Currently translated at 96.0% (96 of 100 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/sv/
2022-09-16 13:06:26 +02:00
Ben Phelps
ddb2a74540 add AdGuard widget 2022-09-16 14:05:56 +03:00
Ben Phelps
37d8d7a2f8 fix indentation 2022-09-16 14:05:43 +03:00
Ben Phelps
578b715a1f allow HTTP basic auth on generic proxy 2022-09-16 14:05:27 +03:00
Anonymous
f14a811ce9 Translated using Weblate (Swedish)
Currently translated at 100.0% (0 of 0 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/sv/
2022-09-16 11:02:49 +02:00
SuperDOS
06dd6d2213 Added translation using Weblate (Swedish) 2022-09-16 11:02:42 +02:00
Pacux
72471c47f4 Translated using Weblate (Catalan)
Currently translated at 100.0% (100 of 100 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ca/
2022-09-16 10:56:30 +02:00
Nonoss117
aec5f7173c Translated using Weblate (French)
Currently translated at 100.0% (100 of 100 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/fr/
2022-09-16 10:56:29 +02:00
Ángel Fernández Sánchez
f7b68789ac Translated using Weblate (Spanish)
Currently translated at 100.0% (100 of 100 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/es/
2022-09-16 10:56:29 +02:00
Ángel Fernández Sánchez
0672da621e Translated using Weblate (Spanish)
Currently translated at 100.0% (100 of 100 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/es/
2022-09-16 10:40:37 +02:00
Juan Manuel Bennàssar Carretero
a7f9b78533 Translated using Weblate (Spanish)
Currently translated at 100.0% (100 of 100 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/es/
2022-09-16 10:40:36 +02:00
Ben Phelps
0075429e08 add greeting and datetime info widgets 2022-09-16 10:53:12 +03:00
Ben Phelps
43f7ccd166 update readme attributions 2022-09-16 10:49:20 +03:00
Ángel Fernández Sánchez
8c64e0f288 Translated using Weblate (Spanish)
Currently translated at 100.0% (100 of 100 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/es/
2022-09-16 09:10:38 +02:00
Juan Manuel Bennàssar Carretero
c91a387833 Translated using Weblate (Spanish)
Currently translated at 100.0% (100 of 100 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/es/
2022-09-16 09:10:38 +02:00
Ben Phelps
93d5dd88ba add options for layout, theme and color settings 2022-09-15 19:58:41 +03:00
Ben Phelps
05427253b9 tweak streaming widget spacings 2022-09-15 19:53:48 +03:00
Ben Phelps
e2bc541089 show transcoding info on streaming widgets 2022-09-15 19:48:23 +03:00
Kamil Ganczarek
9a959bab16 Translated using Weblate (Polish)
Currently translated at 89.0% (89 of 100 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/pl/
2022-09-15 11:30:42 +02:00
Anonymous
45ca4a15f7 Translated using Weblate (Polish)
Currently translated at 100.0% (0 of 0 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/pl/
2022-09-15 11:01:12 +02:00
Kamil Ganczarek
ddd2ff53ff Added translation using Weblate (Polish) 2022-09-15 11:01:05 +02:00
Nonoss117
5c3266b48f Translated using Weblate (French)
Currently translated at 100.0% (100 of 100 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/fr/
2022-09-15 10:58:49 +02:00
Ben Phelps
0da6db9d9f update readme with supported integrations 2022-09-15 09:00:40 +03:00
58 changed files with 1740 additions and 277 deletions

19
.vscode/launch.json vendored Normal file
View 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"
}
}
]
}

View File

@@ -9,16 +9,16 @@
- Images built for AMD64 (x86_64), ARM64, ARMv7 and ARMv6
- Supports all Raspberry Pi's, most SBCs & Apple Silicon
- Full i18n support with automatic language detection
- Translations for Chinese, Dutch, French, German, Norwegian Bokmål, Portuguese, Russian and Spanish
- Translations for Chinese, Dutch, French, German, Hebrew, Hungarian, Norwegian Bokmål, Polish, Portuguese, Russian, Spanish and Swedish
- Want to help translate? [Join the Weblate project](https://hosted.weblate.org/engage/homepage/)
- Service & Web Bookmarks
- Docker Integration
- Container status (Running / Stopped) & statistics (CPU, Memory, Network)
- Automatic service discovery (via labels)
- Service Integration
- Sonarr, Radarr, Readarr, Prowlarr, Emby, Jellyfin, Tautulli (Plex)
- Ombi, Overseerr, Jellyseerr, NZBGet, SABnzbd, ruTorrent, Transmission
- Portainer, Traefik, Speedtest Tracker, PiHole, Nginx Proxy Manager, Gotify
- Sonarr, Radarr, Readarr, Prowlarr, Bazarr, Lidarr, Emby, Jellyfin, Tautulli (Plex)
- Ombi, Overseerr, Jellyseerr, Jackett, NZBGet, SABnzbd, ruTorrent, Transmission, qBittorrent
- Portainer, Traefik, Speedtest Tracker, PiHole, AdGuard Home, Nginx Proxy Manager, Gotify
- Information Providers
- Coin Market Cap
- Information & Utility Widgets
@@ -50,11 +50,10 @@ services:
homepage:
image: ghcr.io/benphelps/homepage:latest
container_name: homepage
user: 1000:1000 # Optional, change to your user and group IDs for permissions
ports:
- 3000:3000
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
```
@@ -121,17 +120,24 @@ Huge thanks to the all the contributors who have helped make this project what i
- [aidenpwnz](https://github.com/benphelps/homepage/commits?author=aidenpwnz) - Nginx Proxy Manager, Search Bar Widget
- [AlexFullmoon](https://github.com/benphelps/homepage/commits?author=AlexFullmoon) - OpenWeatherMap Widget
- [AmadeusGraves](https://github.com/benphelps/homepage/commits?author=AmadeusGraves) - Spanish Translation
- [andrii-kryvoviaz](https://github.com/benphelps/homepage/commits?author=andrii-kryvoviaz) - Background opacity option
- [boerniee](https://github.com/benphelps/homepage/commits?author=boerniee) - German Translation
- [comradekingu](https://github.com/benphelps/homepage/commits?author=comradekingu) - Norwegian Bokmål Translation
- [Daniel Varga] - German & Hungarian Translation
- [deffcolony](https://github.com/benphelps/homepage/commits?author=deffcolony) - Dutch Translation
- [desolaris](https://github.com/benphelps/homepage/commits?author=desolaris) - Russian Translation
- [ilusi0n](https://github.com/benphelps/homepage/commits?author=ilusi0n) - Jellyseerr Integration
- [ItsJustMeChris](https://github.com/benphelps/homepage/commits?author=ItsJustMeChris) - Coin Market Cap Widget
- [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 & qBittorrent 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
- [nicedc](https://github.com/benphelps/homepage/commits?author=nicedc) - Chinese 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
- [quod](https://github.com/benphelps/homepage/commits?author=quod) - Fixed Typos
- [schklom](https://github.com/benphelps/homepage/commits?author=schklom) - ARM64, ARMv7 and ARMv6
- [ShlomiPorush](https://github.com/benphelps/homepage/commits?author=ShlomiPorush) - Hebrew Translation
- [SuperDOS](https://github.com/benphelps/homepage/commits?author=SuperDOS) - Swedish Translation
- [xicopitz](https://github.com/benphelps/homepage/commits?author=xicopitz) - Gotify & Prowlarr Integration

View File

@@ -97,7 +97,7 @@
"pihole": {
"queries": "Consultes",
"blocked": "Bloquejat",
"gravity": "Gravetat"
"gravity": "Gravity"
},
"portainer": {
"running": "Executant",
@@ -134,16 +134,28 @@
"numberOfFailQueries": "Consultes fallides"
},
"jackett": {
"configured": "Configured",
"errored": "Errored"
"configured": "Configurat",
"errored": "Amb errors"
},
"bazarr": {
"missingEpisodes": "Missing Episodes",
"missingMovies": "Missing Movies"
"missingEpisodes": "Episodis que falten",
"missingMovies": "Pel·lícules que falten"
},
"lidarr": {
"wanted": "Wanted",
"queued": "Queued",
"albums": "Albums"
"wanted": "Volgut",
"queued": "En cua",
"albums": "Àlbums"
},
"adguard": {
"queries": "Consultes",
"blocked": "Bloquejat",
"filtered": "Filtrat",
"latency": "Latència"
},
"qbittorrent": {
"download": "Descàrrega",
"upload": "Càrrega",
"leech": "Leech",
"seed": "Seed"
}
}

View File

@@ -11,7 +11,7 @@
"total": "Gesamt",
"free": "Frei",
"used": "Gebraucht",
"load": "Load"
"load": "Belastung"
},
"docker": {
"rx": "Rx",
@@ -24,13 +24,13 @@
"playing": "Spielen",
"transcoding": "Transcodierung",
"bitrate": "Bitrate",
"no_active": "No Active Streams"
"no_active": "Keine aktive Streams"
},
"tautulli": {
"playing": "Spielen",
"transcoding": "Transcodierung",
"bitrate": "Bitrate",
"no_active": "No Active Streams"
"no_active": "Keine aktiven streamen"
},
"rutorrent": {
"active": "Aktiv",
@@ -145,5 +145,17 @@
"wanted": "Wanted",
"queued": "Queued",
"albums": "Albums"
},
"adguard": {
"queries": "Queries",
"blocked": "Blocked",
"filtered": "Filtered",
"latency": "Latency"
},
"qbittorrent": {
"download": "Download",
"upload": "Upload",
"leech": "Leech",
"seed": "Seed"
}
}

View File

@@ -70,6 +70,12 @@
"leech": "Leech",
"seed": "Seed"
},
"qbittorrent": {
"download": "Download",
"upload": "Upload",
"leech": "Leech",
"seed": "Seed"
},
"sonarr": {
"wanted": "Wanted",
"queued": "Queued",
@@ -108,12 +114,18 @@
"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",

View File

@@ -21,16 +21,16 @@
"offline": "Desconectado"
},
"emby": {
"playing": "En ejecución",
"playing": "Reproduciendo",
"transcoding": "Transcodificando",
"bitrate": "Tasa de Bits",
"no_active": "No hay streams activos"
"bitrate": "Tasa de bits",
"no_active": "Sin transmisiones activas"
},
"tautulli": {
"playing": "En ejecución",
"transcoding": "Transcodificación",
"playing": "Reproduciendo",
"transcoding": "Transcodificando",
"bitrate": "Tasa de bits",
"no_active": "No hay streams activos"
"no_active": "Sin transmisiones activas"
},
"rutorrent": {
"active": "Activo",
@@ -39,17 +39,17 @@
},
"sonarr": {
"wanted": "Más deseado",
"queued": "Puesto en cola",
"queued": "En cola",
"series": "Series"
},
"radarr": {
"wanted": "Más deseado",
"queued": "Puesto en cola",
"queued": "En cola",
"movies": "Películas"
},
"readarr": {
"wanted": "Más deseado",
"queued": "Puesto en cola",
"queued": "En cola",
"books": "Libros"
},
"ombi": {
@@ -68,7 +68,7 @@
"gravity": "Gravedad"
},
"speedtest": {
"upload": "Subir",
"upload": "Subida",
"download": "Descarga",
"ping": "Ping"
},
@@ -88,8 +88,8 @@
"total": "Total"
},
"weather": {
"current": "Ubicación Actual",
"allow": "Haga clic para permitir",
"current": "Localización Actual",
"allow": "Clic para permitir",
"updating": "Actualizando",
"wait": "Espere, por favor"
},
@@ -99,21 +99,21 @@
"available": "Disponible"
},
"sabnzbd": {
"rate": "Tasa de descarga",
"queue": "Puesto en cola",
"timeleft": "Tiempo Restante"
"rate": "Tasa",
"queue": "En cola",
"timeleft": "Tiempo restante"
},
"nzbget": {
"rate": "Tasa de descarga",
"rate": "Tasa",
"remaining": "Restante",
"downloaded": "Descargado"
},
"coinmarketcap": {
"configure": "Configurar una o varias criptomonedas para su seguimiento",
"configure": "Configurar una o s criptomonedas para rastrear",
"1hour": "1 Hora",
"1day": "1 Día",
"7days": "7 Dias",
"30days": "30 Dias"
"7days": "7 Días",
"30days": "30 Días"
},
"gotify": {
"apps": "Aplicaciones",
@@ -124,26 +124,38 @@
"enableIndexers": "Indexadores",
"numberOfGrabs": "Capturas",
"numberOfQueries": "Consultas",
"numberOfFailGrabs": "Capturas Fallidas",
"numberOfFailQueries": "Consultas Fallidas"
"numberOfFailGrabs": "Capturas fallidas",
"numberOfFailQueries": "Consultas fallidas"
},
"transmission": {
"download": "Descarga",
"upload": "Subida",
"leech": "Egoístas (Leech)",
"leech": "Compañeros",
"seed": "Semillas"
},
"jackett": {
"configured": "Configured",
"errored": "Errored"
"configured": "Configurado",
"errored": "Con errores"
},
"bazarr": {
"missingEpisodes": "Missing Episodes",
"missingMovies": "Missing Movies"
"missingEpisodes": "Episodios perdidos",
"missingMovies": "Películas perdidas"
},
"lidarr": {
"queued": "Queued",
"wanted": "Wanted",
"albums": "Albums"
"queued": "En cola",
"wanted": "Más deseado",
"albums": "Álbumes"
},
"adguard": {
"queries": "Consultas",
"blocked": "Bloqueado",
"filtered": "Filtrado",
"latency": "Latencia"
},
"qbittorrent": {
"download": "Descarga",
"upload": "Subida",
"leech": "Compañeros",
"seed": "Semillas"
}
}

View File

@@ -34,8 +34,8 @@
},
"rutorrent": {
"active": "Actif",
"upload": "Téléverser",
"download": "Télécharger"
"upload": "Envoi",
"download": "Réception"
},
"sonarr": {
"wanted": "Demandé",
@@ -68,8 +68,8 @@
"gravity": "Listes dom. bloqués"
},
"speedtest": {
"upload": "Téléversement",
"download": "Téléchargement",
"upload": "Envoi",
"download": "Récep.",
"ping": "Ping"
},
"portainer": {
@@ -106,7 +106,7 @@
},
"overseerr": {
"pending": "En attente",
"approved": "Validé",
"approved": "Demande",
"available": "Disponible"
},
"sabnzbd": {
@@ -127,9 +127,9 @@
"30days": "30 Jours"
},
"gotify": {
"apps": "Applications",
"apps": "Applis",
"clients": "Clients",
"messages": "Messages"
"messages": "Msg"
},
"prowlarr": {
"enableIndexers": "Indexeurs",
@@ -149,12 +149,24 @@
"errored": "En erreur"
},
"bazarr": {
"missingEpisodes": "Missing Episodes",
"missingMovies": "Missing Movies"
"missingEpisodes": "Épisodes manquants",
"missingMovies": "Films manquants"
},
"lidarr": {
"wanted": "Wanted",
"queued": "Queued",
"wanted": "Demandé",
"queued": "En queue",
"albums": "Albums"
},
"adguard": {
"queries": "Requêtes",
"blocked": "Bloquées",
"filtered": "Filtrées",
"latency": "Latence"
},
"qbittorrent": {
"download": "Récep.",
"upload": "Envoi",
"leech": "Leech",
"seed": "Seed"
}
}

View File

@@ -0,0 +1,161 @@
{
"widget": {
"missing_type": "סוג ווידג'ט חסר: {{type}}",
"api_error": "שגיאת API",
"status": "סטטוס"
},
"weather": {
"current": "מיקום נוכחי",
"allow": "יש ללחוץ כדי לאשר",
"updating": "מעדכן",
"wait": "המתן בבקשה"
},
"search": {
"placeholder": "חיפוש…"
},
"resources": {
"total": "סה\"כ",
"free": "פנוי",
"used": "בשימוש",
"load": "עומס"
},
"docker": {
"rx": "RX",
"tx": "TX",
"mem": "זיכרון",
"cpu": "מעבד",
"offline": "כבוי"
},
"emby": {
"playing": "מנגן",
"transcoding": "מקודד",
"bitrate": "סיביות",
"no_active": "אין הזרמות פעילות"
},
"tautulli": {
"playing": "מנגן",
"transcoding": "מקודד",
"bitrate": "סיביות",
"no_active": "אין הזרמות פעילות"
},
"nzbget": {
"rate": "יחס",
"remaining": "נותר",
"downloaded": "הורד"
},
"sabnzbd": {
"rate": "יחס",
"queue": "תור",
"timeleft": "זמן שנותר"
},
"rutorrent": {
"active": "פעיל",
"upload": "העלאה",
"download": "הורדה"
},
"transmission": {
"download": "הורדה",
"upload": "העלאה",
"leech": "בהורדה",
"seed": "בשיתוף"
},
"qbittorrent": {
"download": "הורדה",
"upload": "העלאה",
"leech": "בהורדה",
"seed": "בשיתוף"
},
"sonarr": {
"wanted": "מבוקש",
"queued": "בתור",
"series": "סדרות"
},
"radarr": {
"wanted": "מבוקש",
"queued": "בתור",
"movies": "סרטים"
},
"lidarr": {
"wanted": "מבוקש",
"queued": "בתור",
"albums": "אלבומים"
},
"readarr": {
"wanted": "מבוקש",
"queued": "בתור",
"books": "ספרים"
},
"bazarr": {
"missingEpisodes": "פרקים חסרים",
"missingMovies": "סרטים חסרים"
},
"ombi": {
"pending": "ממתין",
"approved": "מאושר",
"available": "זמין"
},
"jellyseerr": {
"pending": "ממתין",
"approved": "מאושר",
"available": "זמין"
},
"overseerr": {
"pending": "ממתין",
"approved": "מאושר",
"available": "זמין"
},
"pihole": {
"queries": "שאילתות",
"blocked": "נחסם",
"gravity": "Gravity"
},
"adguard": {
"queries": "שאילתות",
"blocked": "נחסם",
"filtered": "מסונן",
"latency": "השהיה"
},
"speedtest": {
"upload": "העלאה",
"download": "הורדה",
"ping": "פינג"
},
"portainer": {
"running": "פעיל",
"stopped": "נעצר",
"total": "סה\"כ"
},
"traefik": {
"routers": "ניתובים",
"services": "שירותים",
"middleware": "מתווך"
},
"npm": {
"enabled": "מופעל",
"disabled": "מבוטל",
"total": "סה\"כ"
},
"coinmarketcap": {
"configure": "קבע את התצורה של מטבע קריפטו אחד או יותר למעקב",
"1hour": "שעה אחת",
"1day": "יום 1",
"7days": "7 יום",
"30days": "30 יום"
},
"gotify": {
"apps": "אפליקציות",
"clients": "לקוחות",
"messages": "הודעות"
},
"prowlarr": {
"enableIndexers": "אינדקסים",
"numberOfGrabs": "Grabs",
"numberOfQueries": "שאילתות",
"numberOfFailGrabs": "Fail Grabs",
"numberOfFailQueries": "Fail Queries"
},
"jackett": {
"configured": "מוגדר",
"errored": "שגיאה"
}
}

View 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"
}
}

View File

@@ -0,0 +1,161 @@
{
"resources": {
"total": "Összes",
"free": "Szabad",
"used": "Használt",
"load": "Terhelés"
},
"docker": {
"rx": "RX",
"tx": "TX",
"mem": "MEM",
"cpu": "CPU",
"offline": "Offline"
},
"lidarr": {
"albums": "Albumok",
"wanted": "Keresett",
"queued": "Sorban áll"
},
"readarr": {
"wanted": "Keresett",
"queued": "Sorban áll",
"books": "Könyvek"
},
"bazarr": {
"missingEpisodes": "Hiányzó epizódok",
"missingMovies": "Hiányzó filmek"
},
"widget": {
"missing_type": "Hiányzó Widget Típus: {{type}}",
"api_error": "API Hiba",
"status": "Státusz"
},
"weather": {
"current": "Aktuális hely",
"allow": "Kattints az engedélyezéshez",
"updating": "Frissítés",
"wait": "Kérlek várj"
},
"search": {
"placeholder": "Keresés…"
},
"emby": {
"playing": "Lejátszás",
"transcoding": "Átkódolás",
"bitrate": "Bitráta",
"no_active": "Nincs aktív lejátszás"
},
"tautulli": {
"playing": "Lejátszás folyamatban",
"transcoding": "Átkódolás",
"bitrate": "Bitráta",
"no_active": "Nincs aktív lejátszás"
},
"nzbget": {
"rate": "Ráta",
"remaining": "Hátralévő",
"downloaded": "Letöltött"
},
"sabnzbd": {
"rate": "Ráta",
"queue": "Sor",
"timeleft": "Hátralévő idő"
},
"rutorrent": {
"active": "Aktív",
"upload": "Feltöltés",
"download": "Letöltés"
},
"transmission": {
"leech": "Leechelés",
"seed": "Seedelés",
"download": "Letöltés",
"upload": "Feltöltés"
},
"qbittorrent": {
"download": "Letöltés",
"upload": "Feltöltés",
"leech": "Leechelés",
"seed": "Seedelés"
},
"sonarr": {
"wanted": "Keresett",
"queued": "Sorban áll",
"series": "Sorozat"
},
"radarr": {
"wanted": "Keresett",
"queued": "Sorban áll",
"movies": "Filmek"
},
"ombi": {
"pending": "Függőben",
"approved": "Engedélyezett",
"available": "Elérhető"
},
"jellyseerr": {
"pending": "Függőben",
"approved": "Engedélyezett",
"available": "Elérhető"
},
"overseerr": {
"pending": "Függőben",
"approved": "Engedélyezett",
"available": "Elérhető"
},
"pihole": {
"queries": "Lekérdezések",
"blocked": "Blokkolt",
"gravity": "Gravitáció"
},
"adguard": {
"queries": "Lekérdezések",
"blocked": "Blokkolt",
"filtered": "Szűrt",
"latency": "Késleltetés"
},
"speedtest": {
"upload": "Feltöltés",
"download": "Letöltés",
"ping": "Ping"
},
"portainer": {
"running": "Futó",
"stopped": "Megállított",
"total": "Összes"
},
"traefik": {
"routers": "Routerek",
"services": "Folyamatok",
"middleware": "Közvetítő"
},
"npm": {
"enabled": "Bekapcsolva",
"disabled": "Kikapcsolva",
"total": "Összes"
},
"coinmarketcap": {
"configure": "Állíts be egy vagy több Cryptovalutát a követéshez",
"1hour": "1 Óra",
"1day": "1 Nap",
"7days": "7 Nap",
"30days": "30 Nap"
},
"gotify": {
"apps": "Applikációk",
"clients": "Kliensek",
"messages": "Üzenetek"
},
"prowlarr": {
"enableIndexers": "Indexerek",
"numberOfGrabs": "Fogott",
"numberOfFailGrabs": "Hibás fogások",
"numberOfQueries": "Lekérdezések",
"numberOfFailQueries": "Hibás lekérdezések"
},
"jackett": {
"configured": "Beállított",
"errored": "Hibás"
}
}

View File

@@ -99,14 +99,14 @@
"available": "Disponibili"
},
"sabnzbd": {
"rate": "Rate",
"queue": "Queue",
"timeleft": "Time Left"
"rate": "Rapporto",
"queue": "Coda",
"timeleft": "Tempo Rimanente"
},
"nzbget": {
"rate": "Rate",
"remaining": "Remaining",
"downloaded": "Downloaded"
"rate": "Rapporto",
"remaining": "Rimanente",
"downloaded": "Scaricato"
},
"coinmarketcap": {
"configure": "Configure one or more crypto currencies to track",
@@ -116,9 +116,9 @@
"30days": "30 Days"
},
"gotify": {
"apps": "Applications",
"apps": "Applicazioni",
"clients": "Clients",
"messages": "Messages"
"messages": "Messaggi"
},
"prowlarr": {
"enableIndexers": "Indexers",
@@ -145,5 +145,17 @@
"wanted": "Wanted",
"queued": "Queued",
"albums": "Albums"
},
"adguard": {
"queries": "Queries",
"blocked": "Blocked",
"filtered": "Filtered",
"latency": "Latency"
},
"qbittorrent": {
"download": "Download",
"leech": "Leech",
"upload": "Upload",
"seed": "Seed"
}
}

View File

@@ -145,5 +145,17 @@
"wanted": "Wanted",
"queued": "Queued",
"albums": "Albums"
},
"adguard": {
"queries": "Queries",
"blocked": "Blocked",
"filtered": "Filtered",
"latency": "Latency"
},
"qbittorrent": {
"download": "Download",
"upload": "Upload",
"leech": "Leech",
"seed": "Seed"
}
}

View File

@@ -40,7 +40,7 @@
"playing": "Afspelen",
"transcoding": "Transcodering",
"bitrate": "Bitsnelheid",
"no_active": "No Active Streams"
"no_active": "Geen Actieve Steams"
},
"tautulli": {
"playing": "Afspelen",
@@ -145,5 +145,17 @@
"wanted": "Wanted",
"queued": "Queued",
"albums": "Albums"
},
"adguard": {
"queries": "Queries",
"blocked": "Blocked",
"filtered": "Filtered",
"latency": "Latency"
},
"qbittorrent": {
"download": "Download",
"upload": "Upload",
"leech": "Leech",
"seed": "Seed"
}
}

View File

@@ -0,0 +1,161 @@
{
"weather": {
"allow": "Kliknij, aby zezwolić",
"updating": "Aktualizacja",
"wait": "Proszę czekać",
"current": "Aktualna lokalizacja"
},
"search": {
"placeholder": "Szukaj…"
},
"resources": {
"used": "Użyte",
"load": "Obciążenie",
"total": "Całkowite",
"free": "Wolne"
},
"emby": {
"no_active": "Brak aktywnych strumieni",
"playing": "Odtwarzanie",
"transcoding": "Transkodowanie",
"bitrate": "Bitrate"
},
"tautulli": {
"playing": "Odtwarzanie",
"transcoding": "Transkodowanie",
"bitrate": "Bitrate",
"no_active": "Brak aktywnych strumieni"
},
"speedtest": {
"download": "Pobieranie",
"ping": "Ping",
"upload": "Wysyłanie"
},
"portainer": {
"running": "Działające",
"stopped": "Zatrzymane",
"total": "Ogólnie"
},
"coinmarketcap": {
"1day": "1 dzień",
"7days": "7 dni",
"30days": "30 dni",
"1hour": "1 godzina",
"configure": "Wybierz jedną lub więcej kryptowalut do śledzenia"
},
"gotify": {
"apps": "Aplikacje",
"clients": "Klienci",
"messages": "Wiadomości"
},
"widget": {
"missing_type": "Brakujący typ widżetu: {{type}}",
"api_error": "Błąd API",
"status": "Stan"
},
"docker": {
"rx": "RX",
"tx": "TX",
"mem": "MEM",
"cpu": "CPU",
"offline": "Offline"
},
"nzbget": {
"rate": "Szybkość",
"remaining": "Pozostało",
"downloaded": "Pobrano"
},
"sabnzbd": {
"rate": "Szybkość",
"queue": "Kolejka",
"timeleft": "Pozostało"
},
"rutorrent": {
"active": "Aktywny",
"upload": "Wysyłanie",
"download": "Pobieranie"
},
"transmission": {
"download": "Pobieranie",
"upload": "Wysyłanie",
"leech": "Leech",
"seed": "Seed"
},
"sonarr": {
"wanted": "Poszukiwane",
"queued": "W kolejce",
"series": "Seriale"
},
"radarr": {
"wanted": "Poszukiwane",
"queued": "W kolejce",
"movies": "Filmy"
},
"lidarr": {
"wanted": "Poszukiwane",
"queued": "W kolejce",
"albums": "Albumy"
},
"readarr": {
"wanted": "Poszukiwane",
"queued": "W kolejce",
"books": "Książki"
},
"bazarr": {
"missingEpisodes": "Brakujące odcinki",
"missingMovies": "Brakujące filmy"
},
"ombi": {
"pending": "Oczekiwane",
"approved": "Zaakceptowane",
"available": "Dostępne"
},
"jellyseerr": {
"pending": "Oczekiwane",
"approved": "Zaakceptowane",
"available": "Dostępne"
},
"overseerr": {
"pending": "Oczekiwane",
"approved": "Zaakceptowane",
"available": "Dostępne"
},
"pihole": {
"queries": "Zapytania",
"blocked": "Zablokowane",
"gravity": "Gravity"
},
"traefik": {
"routers": "Routery",
"services": "Serwisy",
"middleware": "Pośrednicy"
},
"npm": {
"enabled": "Włączone",
"disabled": "Wyłączone",
"total": "Ogólnie"
},
"prowlarr": {
"enableIndexers": "Indeksery",
"numberOfGrabs": "Pochwycenia",
"numberOfQueries": "Zapytania",
"numberOfFailGrabs": "Nieudane pochwycenia",
"numberOfFailQueries": "Nieudane zapytania"
},
"jackett": {
"configured": "Skonfigurowane",
"errored": "Błędne"
},
"adguard": {
"queries": "Queries",
"blocked": "Blocked",
"filtered": "Filtered",
"latency": "Latency"
},
"qbittorrent": {
"download": "Download",
"upload": "Upload",
"leech": "Leech",
"seed": "Seed"
}
}

View File

@@ -156,5 +156,17 @@
"queued": "Queued",
"wanted": "Wanted",
"albums": "Albums"
},
"adguard": {
"queries": "Queries",
"blocked": "Blocked",
"filtered": "Filtered",
"latency": "Latency"
},
"qbittorrent": {
"download": "Download",
"upload": "Upload",
"leech": "Leech",
"seed": "Seed"
}
}

View File

@@ -113,7 +113,7 @@
"1hour": "1 Hour",
"1day": "1 Day",
"7days": "7 Days",
"30days": "30 Days"
"30days": "30 Дней"
},
"gotify": {
"apps": "Applications",
@@ -145,5 +145,17 @@
"wanted": "Wanted",
"queued": "Queued",
"albums": "Albums"
},
"adguard": {
"queries": "Queries",
"blocked": "Blocked",
"filtered": "Filtered",
"latency": "Latency"
},
"qbittorrent": {
"download": "Download",
"upload": "Upload",
"leech": "Leech",
"seed": "Seed"
}
}

View File

@@ -0,0 +1,161 @@
{
"widget": {
"missing_type": "Saknar Widget-typ: {{type}}",
"api_error": "API-fel",
"status": "Status"
},
"weather": {
"current": "Nuvarande plats",
"allow": "Klicka för att tillåta",
"updating": "Uppdaterar",
"wait": "Vänligen vänta"
},
"resources": {
"load": "Laddar",
"total": "Total",
"free": "Ledigt",
"used": "Använt"
},
"docker": {
"rx": "RX",
"tx": "TX",
"mem": "MEM",
"cpu": "CPU",
"offline": "Offline"
},
"search": {
"placeholder": "Sök…"
},
"emby": {
"playing": "Spelar",
"transcoding": "Omkodning",
"bitrate": "Bitrate",
"no_active": "Inga aktiva strömmar"
},
"tautulli": {
"playing": "Spelar",
"transcoding": "Omkodning",
"bitrate": "Bitrate",
"no_active": "Inga aktiva strömmar"
},
"nzbget": {
"rate": "Hastighet",
"remaining": "Återstående",
"downloaded": "Nedladdat"
},
"sabnzbd": {
"rate": "Hastighet",
"queue": "Kö",
"timeleft": "Tid kvar"
},
"rutorrent": {
"active": "Aktiva",
"upload": "Uppladdning",
"download": "Nedladdning"
},
"transmission": {
"download": "Nedladdning",
"upload": "Uppladdning",
"leech": "Leech",
"seed": "Seed"
},
"sonarr": {
"wanted": "Eftersöker",
"queued": "I kö",
"series": "Serier"
},
"radarr": {
"wanted": "Eftersöker",
"queued": "I kö",
"movies": "Filmer"
},
"lidarr": {
"wanted": "Eftersöker",
"queued": "I kö",
"albums": "Album"
},
"readarr": {
"wanted": "Eftersökt",
"queued": "I kö",
"books": "Böcker"
},
"bazarr": {
"missingEpisodes": "Saknade program",
"missingMovies": "Saknade filmer"
},
"ombi": {
"pending": "Avvaktar",
"approved": "Godkända",
"available": "Tillgänglig"
},
"jellyseerr": {
"pending": "Avvaktar",
"approved": "Godkända",
"available": "Tillgänglig"
},
"overseerr": {
"pending": "Avvaktar",
"approved": "Godkända",
"available": "Tillgänglig"
},
"pihole": {
"blocked": "Blockerad",
"queries": "Förfrågningar",
"gravity": "Gravity"
},
"speedtest": {
"upload": "Uppladdning",
"download": "Nedladdning",
"ping": "Svarstid"
},
"portainer": {
"running": "Körs",
"stopped": "Stoppade",
"total": "Totalt"
},
"traefik": {
"routers": "Routers",
"services": "Tjänster",
"middleware": "Middleware"
},
"npm": {
"enabled": "Aktiverad",
"disabled": "Inaktiverad",
"total": "Totalt"
},
"coinmarketcap": {
"configure": "Konfigurera en eller flera kryptovalutor att följa",
"1hour": "1 timme",
"1day": "1 dag",
"7days": "7 dagar",
"30days": "30 dagar"
},
"gotify": {
"apps": "Program",
"clients": "Klienter",
"messages": "Meddelande"
},
"prowlarr": {
"enableIndexers": "Indexerare",
"numberOfGrabs": "Hämtningar",
"numberOfQueries": "Queries",
"numberOfFailGrabs": "Misslyckade hämtningar",
"numberOfFailQueries": "Fail Queries"
},
"jackett": {
"configured": "Konfigurerade",
"errored": "Felaktiga"
},
"adguard": {
"queries": "Queries",
"blocked": "Blocked",
"filtered": "Filtered",
"latency": "Latency"
},
"qbittorrent": {
"download": "Download",
"upload": "Upload",
"leech": "Leech",
"seed": "Seed"
}
}

View File

@@ -145,5 +145,17 @@
"wanted": "Wanted",
"queued": "Queued",
"albums": "Albums"
},
"adguard": {
"queries": "Queries",
"blocked": "Blocked",
"filtered": "Filtered",
"latency": "Latency"
},
"qbittorrent": {
"download": "Download",
"upload": "Upload",
"leech": "Leech",
"seed": "Seed"
}
}

View File

@@ -2,7 +2,7 @@
"widget": {
"missing_type": "缺少小部件类型:{{type}}",
"api_error": "API错误",
"status": "地位"
"status": "状态"
},
"search": {
"placeholder": "搜索…"
@@ -21,13 +21,13 @@
"offline": "离线"
},
"emby": {
"playing": "正在播放",
"playing": "播放",
"transcoding": "转码",
"bitrate": "比特率",
"no_active": "暂无播放"
},
"tautulli": {
"playing": "正在播放",
"playing": "播放",
"transcoding": "转码",
"bitrate": "比特率",
"no_active": "暂无播放"
@@ -38,12 +38,12 @@
"download": "下载"
},
"sonarr": {
"wanted": "通缉",
"wanted": "想看",
"queued": "排队",
"series": "系列"
},
"radarr": {
"wanted": "订阅",
"wanted": "想看",
"queued": "队列",
"movies": "电影"
},
@@ -74,7 +74,7 @@
},
"portainer": {
"running": "运行中",
"stopped": "停止",
"stopped": "停止",
"total": "总计"
},
"traefik": {
@@ -88,10 +88,10 @@
"total": "全部的"
},
"weather": {
"current": "当前位",
"current": "当前位",
"allow": "点击并允许",
"updating": "更新中",
"wait": "请等待"
"wait": "请稍后"
},
"overseerr": {
"pending": "待办",
@@ -110,10 +110,10 @@
},
"coinmarketcap": {
"configure": "配置一个或多个需要追踪的加密",
"1hour": "1 Hour",
"1day": "1 Day",
"7days": "7 Days",
"30days": "30 Days"
"1hour": "1小时",
"1day": "1",
"7days": "7",
"30days": "30"
},
"gotify": {
"apps": "应用",
@@ -121,29 +121,41 @@
"messages": "信息"
},
"prowlarr": {
"enableIndexers": "Indexers",
"numberOfGrabs": "Grabs",
"numberOfQueries": "Queries",
"numberOfFailGrabs": "Fail Grabs",
"numberOfFailQueries": "Fail Queries"
"enableIndexers": "索引器",
"numberOfGrabs": "抓取",
"numberOfQueries": "查询",
"numberOfFailGrabs": "抓取失败",
"numberOfFailQueries": "查询失败"
},
"transmission": {
"download": "下载",
"upload": "上传",
"leech": "Leech",
"leech": "吸血",
"seed": "做种"
},
"jackett": {
"configured": "Configured",
"errored": "Errored"
"configured": "已配置",
"errored": "出错了"
},
"bazarr": {
"missingEpisodes": "Missing Episodes",
"missingMovies": "Missing Movies"
"missingEpisodes": "缺少的剧集",
"missingMovies": "缺少的电影"
},
"lidarr": {
"wanted": "Wanted",
"queued": "Queued",
"albums": "Albums"
"wanted": "订阅",
"queued": "队列",
"albums": "相册"
},
"adguard": {
"queries": "查询",
"blocked": "阻止",
"filtered": "过滤",
"latency": "延迟"
},
"qbittorrent": {
"download": "下载",
"upload": "上传",
"leech": "吸血",
"seed": "做种"
}
}

View File

@@ -145,5 +145,17 @@
"wanted": "Wanted",
"queued": "Queued",
"albums": "Albums"
},
"adguard": {
"queries": "Queries",
"blocked": "Blocked",
"filtered": "Filtered",
"latency": "Latency"
},
"qbittorrent": {
"download": "Download",
"upload": "Upload",
"leech": "Leech",
"seed": "Seed"
}
}

View File

@@ -61,6 +61,7 @@ export default function ColorToggle() {
{colors.map((color) => (
<button type="button" onClick={() => setColor(color)} key={color}>
<div
title={color}
className={classNames(
active === color ? "border-2" : "border-0",
`rounded-md w-5 h-5 border-black/50 dark:border-white/50 theme-${color} bg-theme-400`

View File

@@ -1,15 +1,18 @@
import classNames from "classnames";
import List from "components/services/list";
export default function ServicesGroup({ services }) {
export default function ServicesGroup({ services, layout }) {
return (
<div
key={services.name}
className="basis-full md:basis-1/2 lg:basis-1/3 xl:basis-1/4 flex-1 p-1"
className={classNames(
layout?.style === "row" ? "basis-full" : "basis-full md:basis-1/2 lg:basis-1/3 xl:basis-1/4",
"flex-1 p-1"
)}
>
<h2 className="text-theme-800 dark:text-theme-300 text-xl font-medium">
{services.name}
</h2>
<List services={services.services} />
<h2 className="text-theme-800 dark:text-theme-300 text-xl font-medium">{services.name}</h2>
<List services={services.services} layout={layout} />
</div>
);
}

View File

@@ -22,12 +22,6 @@ function resolveIcon(icon) {
}
export default function Item({ service }) {
const handleOnClick = () => {
if (service.href && service.href !== "#") {
window.open(service.href, "_blank").focus();
}
};
const hasLink = service.href && service.href !== "#";
return (
@@ -41,13 +35,14 @@ export default function Item({ service }) {
<div className="flex select-none">
{service.icon &&
(hasLink ? (
<button
type="button"
onClick={handleOnClick}
<a
href={service.href}
target="_blank"
rel="noreferrer"
className="flex-shrink-0 flex items-center justify-center w-12 "
>
<Image src={resolveIcon(service.icon)} width={32} height={32} alt="logo" />
</button>
</a>
) : (
<div className="flex-shrink-0 flex items-center justify-center w-12 ">
<Image src={resolveIcon(service.icon)} width={32} height={32} alt="logo" />
@@ -55,16 +50,17 @@ export default function Item({ service }) {
))}
{hasLink ? (
<button
type="button"
onClick={handleOnClick}
<a
href={service.href}
target="_blank"
rel="noreferrer"
className="flex-1 flex items-center justify-between rounded-r-md "
>
<div className="flex-1 px-2 py-2 text-sm text-left">
{service.name}
<p className="text-theme-500 dark:text-theme-400 text-xs font-extralight">{service.description}</p>
</div>
</button>
</a>
) : (
<div className="flex-1 flex items-center justify-between rounded-r-md ">
<div className="flex-1 px-2 py-2 text-sm text-left">

View File

@@ -1,8 +1,27 @@
import classNames from "classnames";
import Item from "components/services/item";
export default function List({ services }) {
const columnMap = [
"grid-cols-1 md:grid-cols-1 lg:grid-cols-1",
"grid-cols-1 md:grid-cols-1 lg:grid-cols-1",
"grid-cols-1 md:grid-cols-2 lg:grid-cols-2",
"grid-cols-1 md:grid-cols-2 lg:grid-cols-3",
"grid-cols-1 md:grid-cols-2 lg:grid-cols-4",
"grid-cols-1 md:grid-cols-2 lg:grid-cols-5",
"grid-cols-1 md:grid-cols-2 lg:grid-cols-6",
"grid-cols-1 md:grid-cols-2 lg:grid-cols-7",
"grid-cols-1 md:grid-cols-2 lg:grid-cols-8",
];
export default function List({ services, layout }) {
return (
<ul className="mt-3 flex flex-col">
<ul
className={classNames(
layout?.style === "row" ? `grid ${columnMap[layout?.columns]} gap-x-2` : "flex flex-col",
"mt-3"
)}
>
{services.map((service) => (
<Item key={service.name} service={service} />
))}

View File

@@ -11,6 +11,7 @@ import Emby from "./widgets/service/emby";
import Nzbget from "./widgets/service/nzbget";
import SABnzbd from "./widgets/service/sabnzbd";
import Transmission from "./widgets/service/transmission";
import QBittorrent from "./widgets/service/qbittorrent";
import Docker from "./widgets/service/docker";
import Pihole from "./widgets/service/pihole";
import Rutorrent from "./widgets/service/rutorrent";
@@ -25,6 +26,7 @@ import CoinMarketCap from "./widgets/service/coinmarketcap";
import Gotify from "./widgets/service/gotify";
import Prowlarr from "./widgets/service/prowlarr";
import Jackett from "./widgets/service/jackett";
import AdGuard from "./widgets/service/adguard";
const widgetMappings = {
docker: Docker,
@@ -40,6 +42,7 @@ const widgetMappings = {
nzbget: Nzbget,
sabnzbd: SABnzbd,
transmission: Transmission,
qbittorrent: QBittorrent,
pihole: Pihole,
rutorrent: Rutorrent,
speedtest: Speedtest,
@@ -52,6 +55,7 @@ const widgetMappings = {
gotify: Gotify,
prowlarr: Prowlarr,
jackett: Jackett,
adguard: AdGuard,
};
export default function Widget({ service }) {

View File

@@ -0,0 +1,45 @@
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 AdGuard({ service }) {
const { t } = useTranslation();
const config = service.widget;
const { data: adguardData, error: adguardError } = useSWR(formatApiUrl(config, "stats"));
if (adguardError) {
return <Widget error={t("widget.api_error")} />;
}
if (!adguardData) {
return (
<Widget>
<Block label={t("adguard.queries")} />
<Block label={t("adguard.blocked")} />
<Block label={t("adguard.filtered")} />
<Block label={t("adguard.latency")} />
</Widget>
);
}
const filtered =
adguardData.num_replaced_safebrowsing + adguardData.num_replaced_safesearch + adguardData.num_replaced_parental;
return (
<Widget>
<Block label={t("adguard.queries")} value={t("common.number", { value: adguardData.num_dns_queries })} />
<Block label={t("adguard.blocked")} value={t("common.number", { value: adguardData.num_blocked_filtering })} />
<Block label={t("adguard.filtered")} value={t("common.number", { value: filtered })} />
<Block
label={t("adguard.latency")}
value={t("common.ms", { value: adguardData.avg_processing_time * 1000, style: "unit", unit: "millisecond" })}
/>
</Widget>
);
}

View File

@@ -29,8 +29,8 @@ export default function Bazarr({ service }) {
return (
<Widget>
<Block label={t("bazarr.missingEpisodes")} value={episodesData.total} />
<Block label={t("bazarr.missingMovies")} value={moviesData.total} />
<Block label={t("bazarr.missingEpisodes")} value={t("common.number", { value: episodesData.total })} />
<Block label={t("bazarr.missingMovies")} value={t("common.number", { value: moviesData.total })} />
</Widget>
);
}

View File

@@ -1,6 +1,7 @@
import useSWR from "swr";
import { useTranslation } from "react-i18next";
import { BsVolumeMuteFill, BsFillPlayFill, BsPauseFill } from "react-icons/bs";
import { BsVolumeMuteFill, BsFillPlayFill, BsPauseFill, BsCpu, BsFillCpuFill } from "react-icons/bs";
import { MdOutlineSmartDisplay } from "react-icons/md";
import Widget from "../widget";
@@ -27,24 +28,35 @@ function ticksToString(ticks) {
}
function SingleSessionEntry({ playCommand, session }) {
console.log(session);
const {
NowPlayingItem: { Name, SeriesName, RunTimeTicks },
PlayState: { PositionTicks, IsPaused, IsMuted },
} = session;
const { IsVideoDirect, VideoDecoderIsHardware, VideoEncoderIsHardware } = session?.TranscodingInfo || {
IsVideoDirect: true,
VideoDecoderIsHardware: true,
VideoEncoderIsHardware: true,
};
const percent = (PositionTicks / RunTimeTicks) * 100;
return (
<>
<div className="text-theme-700 dark:text-theme-200 relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1 flex">
<div className="text-xs z-10 self-center ml-2">
<span>
<div className="grow text-xs z-10 self-center ml-2 relative w-full h-4 mr-2">
<div className="absolute w-full whitespace-nowrap text-ellipsis overflow-hidden">
{Name}
{SeriesName && ` - ${SeriesName}`}
</span>
</div>
</div>
<div className="self-center text-xs flex justify-end mr-1.5 pl-1">
{IsVideoDirect && <MdOutlineSmartDisplay className="opacity-50" />}
{!IsVideoDirect && (!VideoDecoderIsHardware || !VideoEncoderIsHardware) && <BsCpu className="opacity-50" />}
{!IsVideoDirect && VideoDecoderIsHardware && VideoEncoderIsHardware && (
<BsFillCpuFill className="opacity-50" />
)}
</div>
<div className="grow" />
<div className="self-center text-xs flex justify-end mr-1">{IsMuted && <BsVolumeMuteFill />}</div>
</div>
<div className="text-theme-700 dark:text-theme-200 relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1 flex">
@@ -73,7 +85,12 @@ function SingleSessionEntry({ playCommand, session }) {
)}
</div>
<div className="grow " />
<div className="self-center text-xs flex justify-end mr-2">{ticksToString(PositionTicks)}</div>
<div className="self-center text-xs flex justify-end mr-1 z-10">{IsMuted && <BsVolumeMuteFill />}</div>
<div className="self-center text-xs flex justify-end mr-2 z-10">
{ticksToString(PositionTicks)}
<span className="mx-0.5 text-[8px]">/</span>
{ticksToString(RunTimeTicks)}
</div>
</div>
</>
);
@@ -84,6 +101,9 @@ function SessionEntry({ playCommand, session }) {
NowPlayingItem: { Name, SeriesName, RunTimeTicks },
PlayState: { PositionTicks, IsPaused, IsMuted },
} = session;
const { IsVideoDirect, VideoDecoderIsHardware, VideoEncoderIsHardware } = session?.TranscodingInfo || {};
const percent = (PositionTicks / RunTimeTicks) * 100;
return (
@@ -111,14 +131,20 @@ function SessionEntry({ playCommand, session }) {
className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80"
/>
)}
<span>
</div>
<div className="grow text-xs z-10 self-center relative w-full h-4">
<div className="absolute w-full whitespace-nowrap text-ellipsis overflow-hidden">
{Name}
{SeriesName && ` - ${SeriesName}`}
</span>
</div>
</div>
<div className="self-center text-xs flex justify-end mr-1 z-10">{IsMuted && <BsVolumeMuteFill />}</div>
<div className="self-center text-xs flex justify-end mr-1 z-10">{ticksToString(PositionTicks)}</div>
<div className="self-center items-center text-xs flex justify-end mr-1.5 pl-1 z-10">
{IsVideoDirect && <MdOutlineSmartDisplay className="opacity-50" />}
{!IsVideoDirect && (!VideoDecoderIsHardware || !VideoEncoderIsHardware) && <BsCpu className="opacity-50" />}
{!IsVideoDirect && VideoDecoderIsHardware && VideoEncoderIsHardware && <BsFillCpuFill className="opacity-50" />}
</div>
<div className="grow " />
<div className="self-center text-xs flex justify-end mr-1">{IsMuted && <BsVolumeMuteFill />}</div>
<div className="self-center text-xs flex justify-end mr-2">{ticksToString(PositionTicks)}</div>
</div>
);
}

View File

@@ -30,8 +30,8 @@ export default function Jackett({ service }) {
return (
<Widget>
<Block label={t("jackett.configured")} value={indexersData.length} />
<Block label={t("jackett.errored")} value={errored.length} />
<Block label={t("jackett.configured")} value={t("common.number", { value: indexersData.length })} />
<Block label={t("jackett.errored")} value={t("common.number", { value: errored.length })} />
</Widget>
);
}

View File

@@ -29,13 +29,11 @@ export default function Lidarr({ service }) {
);
}
const have = albumsData.filter((album) => album.statistics.percentOfTracks === 100);
return (
<Widget>
<Block label={t("lidarr.wanted")} value={wantedData.totalRecords} />
<Block label={t("lidarr.queued")} value={queueData.totalCount} />
<Block label={t("lidarr.albums")} value={have.length} />
<Block label={t("lidarr.wanted")} value={t("common.number", { value: wantedData.totalRecords })} />
<Block label={t("lidarr.queued")} value={t("common.number", { value: queueData.totalCount })} />
<Block label={t("lidarr.albums")} value={t("common.number", { value: albumsData.have })} />
</Widget>
);
}

View 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>
);
}

View File

@@ -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 (
<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.movies")} value={have.length} />
<Block label={t("radarr.movies")} value={moviesData.have} />
</Widget>
);
}

View File

@@ -29,13 +29,11 @@ export default function Readarr({ service }) {
);
}
const have = booksData.filter((book) => book.statistics.bookFileCount > 0);
return (
<Widget>
<Block label={t("readarr.wanted")} value={wantedData.totalRecords} />
<Block label={t("readarr.queued")} value={queueData.totalCount} />
<Block label={t("readarr.books")} value={have.length} />
<Block label={t("readarr.wanted")} value={t("common.number", { value: wantedData.totalRecords })} />
<Block label={t("readarr.queued")} value={t("common.number", { value: queueData.totalCount })} />
<Block label={t("readarr.books")} value={t("common.number", { value: booksData.have })} />
</Widget>
);
}

View File

@@ -30,7 +30,7 @@ export default function SABnzbd({ service }) {
return (
<Widget>
<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} />
</Widget>
);

View File

@@ -33,7 +33,7 @@ export default function Sonarr({ service }) {
<Widget>
<Block label={t("sonarr.wanted")} value={wantedData.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>
);
}

View File

@@ -1,7 +1,8 @@
/* eslint-disable camelcase */
import useSWR from "swr";
import { useTranslation } from "react-i18next";
import { BsFillPlayFill, BsPauseFill } from "react-icons/bs";
import { BsFillPlayFill, BsPauseFill, BsCpu, BsFillCpuFill } from "react-icons/bs";
import { MdOutlineSmartDisplay, MdSmartDisplay } from "react-icons/md";
import Widget from "../widget";
@@ -27,16 +28,26 @@ function millisecondsToString(milliseconds) {
}
function SingleSessionEntry({ session }) {
const { full_title, duration, view_offset, progress_percent, state, year, grandparent_year } = session;
const { full_title, duration, view_offset, progress_percent, state, video_decision, audio_decision } = session;
return (
<>
<div className="text-theme-700 dark:text-theme-200 relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1 flex">
<div className="text-xs z-10 self-center ml-2">
<span>{full_title}</span>
<div className="text-xs z-10 self-center ml-2 relative w-full h-4 grow mr-2">
<div className="absolute w-full whitespace-nowrap text-ellipsis overflow-hidden">{full_title}</div>
</div>
<div className="self-center text-xs flex justify-end mr-1.5 pl-1">
{video_decision === "direct play" && audio_decision === "direct play" && (
<MdSmartDisplay className="opacity-50" />
)}
{video_decision === "copy" && audio_decision === "copy" && <MdOutlineSmartDisplay className="opacity-50" />}
{video_decision !== "copy" &&
video_decision !== "direct play" &&
(audio_decision !== "copy" || audio_decision !== "direct play") && <BsFillCpuFill className="opacity-50" />}
{(video_decision === "copy" || video_decision === "direct play") &&
audio_decision !== "copy" &&
audio_decision !== "direct play" && <BsCpu className="opacity-50" />}
</div>
<div className="grow" />
<div className="self-center text-xs flex justify-end mr-2">{year || grandparent_year}</div>
</div>
<div className="text-theme-700 dark:text-theme-200 relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1 flex">
@@ -55,8 +66,10 @@ function SingleSessionEntry({ session }) {
)}
</div>
<div className="grow " />
<div className="self-center text-xs flex justify-end mr-2">
{millisecondsToString(view_offset)} / {millisecondsToString(duration)}
<div className="self-center text-xs flex justify-end mr-2 z-10">
{millisecondsToString(view_offset)}
<span className="mx-0.5 text-[8px]">/</span>
{millisecondsToString(duration)}
</div>
</div>
</>
@@ -64,7 +77,7 @@ function SingleSessionEntry({ session }) {
}
function SessionEntry({ session }) {
const { full_title, view_offset, progress_percent, state } = session;
const { full_title, view_offset, progress_percent, state, video_decision, audio_decision } = session;
return (
<div className="text-theme-700 dark:text-theme-200 relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1 flex">
@@ -81,10 +94,23 @@ function SessionEntry({ session }) {
{state !== "paused" && (
<BsFillPlayFill className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80" />
)}
<span>{full_title}</span>
</div>
<div className="grow " />
<div className="self-center text-xs flex justify-end mr-2">{millisecondsToString(view_offset)}</div>
<div className="text-xs z-10 self-center ml-2 relative w-full h-4 grow mr-2">
<div className="absolute w-full whitespace-nowrap text-ellipsis overflow-hidden">{full_title}</div>
</div>
<div className="self-center text-xs flex justify-end mr-1.5 pl-1 z-10">
{video_decision === "direct play" && audio_decision === "direct play" && (
<MdSmartDisplay className="opacity-50" />
)}
{video_decision === "copy" && audio_decision === "copy" && <MdOutlineSmartDisplay className="opacity-50" />}
{video_decision !== "copy" &&
video_decision !== "direct play" &&
(audio_decision !== "copy" || audio_decision !== "direct play") && <BsFillCpuFill className="opacity-50" />}
{(video_decision === "copy" || video_decision === "direct play") &&
audio_decision !== "copy" &&
audio_decision !== "direct play" && <BsCpu className="opacity-50" />}
</div>
<div className="self-center text-xs flex justify-end mr-2 z-10">{millisecondsToString(view_offset)}</div>
</div>
);
}

View File

@@ -61,9 +61,9 @@ export default function Transmission({ service }) {
return (
<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.seed")} value={completed} />
<Block label={t("transmission.seed")} value={t("common.number", { value: completed })} />
<Block label={t("transmission.upload")} value={`${rateUl.toFixed(2)} ${unitsUl}`} />
</Widget>
);

View File

@@ -2,6 +2,8 @@ import WeatherApi from "components/widgets/weather/weather";
import OpenWeatherMap from "components/widgets/openweathermap/weather";
import Resources from "components/widgets/resources/resources";
import Search from "components/widgets/search/search";
import Greeting from "components/widgets/greeting/greeting";
import DateTime from "components/widgets/datetime/datetime";
const widgetMappings = {
weather: WeatherApi, // This key will be deprecated in the future
@@ -9,13 +11,15 @@ const widgetMappings = {
openweathermap: OpenWeatherMap,
resources: Resources,
search: Search,
greeting: Greeting,
datetime: DateTime,
};
export default function Widget({ widget }) {
const ServiceWidget = widgetMappings[widget.type];
const InfoWidget = widgetMappings[widget.type];
if (ServiceWidget) {
return <ServiceWidget options={widget.options} />;
if (InfoWidget) {
return <InfoWidget options={widget.options} />;
}
return (

View File

@@ -0,0 +1,36 @@
import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
const textSizes = {
"4xl": "text-4xl",
"3xl": "text-3xl",
"2xl": "text-2xl",
xl: "text-xl",
lg: "text-lg",
md: "text-md",
sm: "text-sm",
xs: "text-xs",
};
export default function DateTime({ options }) {
const { text_size: textSize, format } = options;
const { i18n } = useTranslation();
const [date, setDate] = useState(new Date());
useEffect(() => {
const interval = setInterval(() => {
setDate(new Date());
}, 1000);
return () => clearInterval(interval);
}, [setDate]);
const dateFormat = new Intl.DateTimeFormat(i18n.language, { ...format });
return (
<div className="flex flex-row items-center grow justify-end">
<span className={`text-theme-800 dark:text-theme-200 ${textSizes[textSize || "lg"]}`}>
{dateFormat.format(date)}
</span>
</div>
);
}

View File

@@ -0,0 +1,22 @@
const textSizes = {
"4xl": "text-4xl",
"3xl": "text-3xl",
"2xl": "text-2xl",
xl: "text-xl",
lg: "text-lg",
md: "text-md",
sm: "text-sm",
xs: "text-xs",
};
export default function Greeting({ options }) {
if (options.text) {
return (
<div className="flex flex-row items-center justify-start">
<span className={`text-theme-800 dark:text-theme-200 ${textSizes[options.text_size || "xl"]}`}>
{options.text}
</span>
</div>
);
}
}

View File

@@ -6,6 +6,8 @@ import "styles/weather-icons.css";
import "styles/theme.css";
import "utils/i18n";
import { ColorProvider } from "utils/color-context";
import { ThemeProvider } from "utils/theme-context";
function MyApp({ Component, pageProps }) {
return (
@@ -14,7 +16,11 @@ function MyApp({ Component, pageProps }) {
fetcher: (resource, init) => fetch(resource, init).then((res) => res.json()),
}}
>
<Component {...pageProps} />
<ColorProvider>
<ThemeProvider>
<Component {...pageProps} />
</ThemeProvider>
</ColorProvider>
</SWRConfig>
);
}

View File

@@ -7,15 +7,17 @@ export default async function handler(req, res) {
try {
discoveredServices = cleanServiceGroups(await servicesFromDocker());
} catch {
console.error("Failed to discover services, please check docker.yaml for errors");
} catch (e) {
console.error("Failed to discover services, please check docker.yaml for errors or remove example entries.");
console.error(e);
discoveredServices = [];
}
try {
configuredServices = cleanServiceGroups(await servicesFromConfig());
} catch {
} catch (e) {
console.error("Failed to load services.yaml, please check for errors");
console.error(e);
configuredServices = [];
}

View File

@@ -4,22 +4,83 @@ import rutorrentProxyHandler from "utils/proxies/rutorrent";
import nzbgetProxyHandler from "utils/proxies/nzbget";
import npmProxyHandler from "utils/proxies/npm";
import transmissionProxyHandler from "utils/proxies/transmission";
import qbittorrentProxyHandler from "utils/proxies/qbittorrent";
function asJson(data) {
if (data?.length > 0) {
const json = JSON.parse(data.toString());
return json;
}
return data;
}
function jsonArrayTransform(data, transform) {
const json = asJson(data);
if (json instanceof Array) {
return transform(json);
}
return json;
}
function jsonArrayFilter(data, filter) {
return jsonArrayTransform(data, (items) => items.filter(filter));
}
const serviceProxyHandlers = {
// uses query param auth
emby: genericProxyHandler,
jellyfin: genericProxyHandler,
pihole: genericProxyHandler,
radarr: genericProxyHandler,
sonarr: genericProxyHandler,
lidarr: genericProxyHandler,
readarr: genericProxyHandler,
bazarr: genericProxyHandler,
radarr: {
proxy: genericProxyHandler,
maps: {
movie: (data) => ({
wanted: jsonArrayFilter(data, (item) => item.isAvailable === false).length,
have: jsonArrayFilter(data, (item) => item.isAvailable === true).length,
}),
},
},
sonarr: {
proxy: genericProxyHandler,
maps: {
series: (data) => ({
total: asJson(data).length,
}),
},
},
lidarr: {
proxy: genericProxyHandler,
maps: {
album: (data) => ({
have: jsonArrayFilter(data, (item) => item?.statistics?.percentOfTracks === 100).length,
}),
},
},
readarr: {
proxy: genericProxyHandler,
maps: {
book: (data) => ({
have: jsonArrayFilter(data, (item) => item?.statistics?.bookFileCount > 0).length,
}),
},
},
bazarr: {
proxy: genericProxyHandler,
maps: {
movies: (data) => ({
total: asJson(data).total,
}),
episodes: (data) => ({
total: asJson(data).total,
}),
},
},
speedtest: genericProxyHandler,
tautulli: genericProxyHandler,
traefik: genericProxyHandler,
sabnzbd: genericProxyHandler,
jackett: genericProxyHandler,
adguard: genericProxyHandler,
// uses X-API-Key (or similar) header auth
gotify: credentialedProxyHandler,
portainer: credentialedProxyHandler,
@@ -33,6 +94,7 @@ const serviceProxyHandlers = {
nzbget: nzbgetProxyHandler,
npm: npmProxyHandler,
transmission: transmissionProxyHandler,
qbittorrent: qbittorrentProxyHandler,
};
export default async function handler(req, res) {
@@ -41,7 +103,14 @@ export default async function handler(req, res) {
const serviceProxyHandler = serviceProxyHandlers[type];
if (serviceProxyHandler) {
return serviceProxyHandler(req, res);
if (serviceProxyHandler instanceof Function) {
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" });

View File

@@ -0,0 +1,9 @@
import checkAndCopyConfig from "utils/config";
const configs = ["docker.yaml", "settings.yaml", "services.yaml", "bookmarks.yaml"];
export default async function handler(req, res) {
const errors = configs.map((config) => checkAndCopyConfig(config)).filter((status) => status !== true);
res.send(errors);
}

View File

@@ -14,7 +14,7 @@ export default async function handler(req, res) {
}
if (!apiKey && provider) {
const settings = await getSettings();
const settings = getSettings();
apiKey = settings?.providers?.openweathermap;
}

View File

@@ -14,7 +14,7 @@ export default async function handler(req, res) {
}
if (!apiKey && provider) {
const settings = await getSettings();
const settings = getSettings();
apiKey = settings?.providers?.weatherapi;
}

View File

@@ -3,15 +3,16 @@ import useSWR from "swr";
import Head from "next/head";
import dynamic from "next/dynamic";
import { useTranslation } from "react-i18next";
import { useEffect } from "react";
import { useEffect, useContext } from "react";
import { BiError } from "react-icons/bi";
import ServicesGroup from "components/services/group";
import BookmarksGroup from "components/bookmarks/group";
import Widget from "components/widget";
import Revalidate from "components/revalidate";
import { getSettings } from "utils/config";
import { ColorProvider } from "utils/color-context";
import { ThemeProvider } from "utils/theme-context";
import { ColorContext } from "utils/color-context";
import { ThemeContext } from "utils/theme-context";
const ThemeToggle = dynamic(() => import("components/theme-toggle"), {
ssr: false,
@@ -21,20 +22,59 @@ const ColorToggle = dynamic(() => import("components/color-toggle"), {
ssr: false,
});
const rightAlignedWidgets = ["weatherapi", "openweathermap", "weather", "search"];
const rightAlignedWidgets = ["weatherapi", "openweathermap", "weather", "search", "datetime"];
export async function getStaticProps() {
const settings = await getSettings();
return {
props: {
settings,
},
};
export function getStaticProps() {
try {
const settings = getSettings();
return {
props: {
settings,
},
};
} catch (e) {
return {
props: {
settings: {},
},
};
}
}
export default function Home({ settings }) {
export default function Index({ settings }) {
const { data: errorsData } = useSWR("/api/validate");
if (errorsData && errorsData.length > 0) {
return (
<div className="w-full container m-auto justify-center p-10">
<div className="flex flex-col">
{errorsData.map((error, i) => (
<div
className="basis-1/2 bg-theme-500 dark:bg-theme-600 text-theme-600 dark:text-theme-300 m-2 rounded-md font-mono shadow-md border-4 border-transparent"
key={i}
>
<div className="bg-amber-200 text-amber-800 dark:text-amber-200 dark:bg-amber-800 p-2 rounded-md font-bold">
<BiError className="float-right w-6 h-6" />
{error.config}
</div>
<div className="p-2 text-theme-100 dark:text-theme-200">
<pre className="opacity-50 font-bold pb-2">{error.reason}</pre>
<pre className="text-sm">{error.mark.snippet}</pre>
</div>
</div>
))}
</div>
</div>
);
}
return <Home settings={settings} />;
}
function Home({ settings }) {
const { i18n } = useTranslation();
const { theme, setTheme } = useContext(ThemeContext);
const { color, setColor } = useContext(ColorContext);
const { data: services } = useSWR("/api/services");
const { data: bookmarks } = useSWR("/api/bookmarks");
@@ -44,67 +84,74 @@ export default function Home({ settings }) {
if (settings.background) {
wrappedStyle.backgroundImage = `url(${settings.background})`;
wrappedStyle.backgroundSize = "cover";
wrappedStyle.opacity = settings.backgroundOpacity ?? 1;
}
useEffect(() => {
if (settings.language) {
i18n.changeLanguage(settings.language);
}
}, [i18n, settings.language]);
if (settings.theme && theme !== settings.theme) {
setTheme(settings.theme);
}
if (settings.color && color !== settings.color) {
setColor(settings.color);
}
}, [i18n, settings, color, setColor, theme, setTheme]);
return (
<ColorProvider>
<ThemeProvider>
<Head>
<title>{settings.title || "Homepage"}</title>
{settings.base && <base href={settings.base} />}
{settings.favicon && <link rel="icon" href={settings.favicon} />}
</Head>
<div className="fixed w-full h-full m-0 p-0" style={wrappedStyle} />
<div className="relative w-full container m-auto flex flex-col h-screen justify-between">
<div className="flex flex-row flex-wrap m-8 pb-4 mt-10 border-b-2 border-theme-800 dark:border-theme-200 justify-between">
{widgets && (
<>
<>
<Head>
<title>{settings.title || "Homepage"}</title>
{settings.base && <base href={settings.base} />}
{settings.favicon && <link rel="icon" href={settings.favicon} />}
</Head>
<div className="fixed w-full h-full m-0 p-0" style={wrappedStyle} />
<div className="relative w-full container m-auto flex flex-col h-screen justify-between">
<div className="flex flex-row flex-wrap m-8 pb-4 mt-10 border-b-2 border-theme-800 dark:border-theme-200 justify-between">
{widgets && (
<>
{widgets
.filter((widget) => !rightAlignedWidgets.includes(widget.type))
.map((widget, i) => (
<Widget key={i} widget={widget} />
))}
<div className="ml-4 flex flex-wrap basis-full grow sm:basis-auto justify-between md:justify-end mt-2 md:mt-0">
{widgets
.filter((widget) => !rightAlignedWidgets.includes(widget.type))
.filter((widget) => rightAlignedWidgets.includes(widget.type))
.map((widget, i) => (
<Widget key={i} widget={widget} />
))}
<div className="ml-4 flex flex-wrap basis-full grow sm:basis-auto justify-between md:justify-end mt-2 md:mt-0">
{widgets
.filter((widget) => rightAlignedWidgets.includes(widget.type))
.map((widget, i) => (
<Widget key={i} widget={widget} />
))}
</div>
</>
)}
</div>
{services && (
<div className="flex flex-wrap p-8 items-start">
{services.map((group) => (
<ServicesGroup key={group.name} services={group} />
))}
</div>
</div>
</>
)}
{bookmarks && (
<div className="grow flex flex-wrap pt-0 p-8">
{bookmarks.map((group) => (
<BookmarksGroup key={group.name} group={group} />
))}
</div>
)}
<div className="rounded-full flex p-8 w-full justify-between">
<ColorToggle />
<Revalidate />
<ThemeToggle />
</div>
</div>
</ThemeProvider>
</ColorProvider>
{services && (
<div className="flex flex-wrap p-8 items-start">
{services.map((group) => (
<ServicesGroup key={group.name} services={group} layout={settings.layout?.[group.name]} />
))}
</div>
)}
{bookmarks && (
<div className="grow flex flex-wrap pt-0 p-8">
{bookmarks.map((group) => (
<BookmarksGroup key={group.name} group={group} />
))}
</div>
)}
<div className="rounded-full flex p-8 w-full justify-end">
{!settings?.color && <ColorToggle />}
<Revalidate />
{!settings?.theme && <ThemeToggle />}
</div>
</div>
</>
);
}

View File

@@ -1,3 +1,4 @@
---
# For configuration options and examples, please see:
# https://github.com/benphelps/homepage/wiki/Bookmarks

View File

@@ -1,9 +1,10 @@
---
# For configuration options and examples, please see:
# https://github.com/benphelps/homepage/wiki/Docker-Integration
my-docker:
host: 127.0.0.1
port: 2375
# my-docker:
# host: 127.0.0.1
# port: 2375
other-docker:
socket: /var/run/docker.sock
# my-docker:
# socket: /var/run/docker.sock

View File

@@ -1,3 +1,4 @@
---
# For configuration options and examples, please see:
# https://github.com/benphelps/homepage/wiki/Services

View File

@@ -1,3 +1,4 @@
---
# For configuration options and examples, please see:
# https://github.com/benphelps/homepage/wiki/Settings

View File

@@ -1,3 +1,4 @@
---
# For configuration options and examples, please see:
# https://github.com/benphelps/homepage/wiki/Information-Widgets

View File

@@ -10,6 +10,7 @@ const formats = {
portainer: `{url}/api/endpoints/{env}/{endpoint}`,
rutorrent: `{url}/plugins/httprpc/action.php`,
transmission: `{url}/transmission/rpc`,
qbittorrent: `{url}/api/v2/{endpoint}`,
jellyseerr: `{url}/api/v1/{endpoint}`,
overseerr: `{url}/api/v1/{endpoint}`,
ombi: `{url}/api/v1/{endpoint}`,
@@ -21,7 +22,8 @@ const formats = {
coinmarketcap: `https://pro-api.coinmarketcap.com/{endpoint}`,
gotify: `{url}/{endpoint}`,
prowlarr: `{url}/api/v1/{endpoint}`,
jackett: `{url}/api/v2.0/{endpoint}?apikey={key}&configured=true`
jackett: `{url}/api/v2.0/{endpoint}?apikey={key}&configured=true`,
adguard: `{url}/control/{endpoint}`,
};
export function formatApiCall(api, args) {

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-console */
import { join } from "path";
import { existsSync, copyFile, promises as fs } from "fs";
import { existsSync, copyFile, readFileSync } from "fs";
import yaml from "js-yaml";
@@ -15,13 +15,22 @@ export default function checkAndCopyConfig(config) {
}
console.info("%s was copied to the config folder", config);
});
return true;
}
try {
yaml.load(readFileSync(configYaml, "utf8"));
return true;
} catch (e) {
return { ...e, config };
}
}
export async function getSettings() {
export function getSettings() {
checkAndCopyConfig("settings.yaml");
const settingsYaml = join(process.cwd(), "config", "settings.yaml");
const fileContents = await fs.readFile(settingsYaml, "utf8");
const fileContents = readFileSync(settingsYaml, "utf8");
return yaml.load(fileContents);
}

34
src/utils/cookie-jar.js Normal file
View 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());
}
}

View File

@@ -1,39 +1,15 @@
/* eslint-disable prefer-promise-reject-errors */
/* eslint-disable no-param-reassign */
import { http, https } from "follow-redirects";
import { Cookie, CookieJar } from 'tough-cookie';
const cookieJar = new CookieJar();
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;
}
}
import { addCookieToJar, setCookieHeader } from "utils/cookie-jar";
function addCookieHandler(url, params) {
setCookieHeader(url, params);
// handle cookies during redirects
params.beforeRedirect = (options, responseInfo) => {
const cookieHeader = responseInfo.headers['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], options.href);
}
addCookieToJar(options.href, responseInfo.headers);
setCookieHeader(options.href, options);
};
}
@@ -49,6 +25,7 @@ export function httpsRequest(url, params) {
});
response.on("end", () => {
addCookieToJar(url, 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", () => {
addCookieToJar(url, response.headers);
resolve([response.statusCode, response.headers["content-type"], Buffer.concat(data), response.headers]);
});
});

View File

@@ -2,7 +2,7 @@ import getServiceWidget from "utils/service-helpers";
import { formatApiCall } from "utils/api-helpers";
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;
if (group && service) {
@@ -10,17 +10,31 @@ export default async function genericProxyHandler(req, res) {
if (widget) {
const url = new URL(formatApiCall(widget.type, { endpoint, ...widget }));
let headers;
if (widget.username && widget.password) {
headers = {
Authorization: `Basic ${Buffer.from(`${widget.username}:${widget.password}`).toString("base64")}`,
};
}
const [status, contentType, data] = await httpProxy(url, {
method: req.method,
headers,
});
let resultData = data;
if (maps?.[endpoint]) {
resultData = maps[endpoint](data);
}
if (contentType) res.setHeader("Content-Type", contentType);
if (status === 204 || status === 304) {
return res.status(status).end();
}
return res.status(status).send(data);
return res.status(status).send(resultData);
}
}

View 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);
}