Compare commits

...

10 Commits

Author SHA1 Message Date
Ben Phelps
939f5d7c20 fix path.join import 2022-09-06 08:39:25 +03:00
Ben Phelps
cf39395924 add documentation link 2022-09-05 20:20:34 +03:00
Ben Phelps
6061d9ec65 cleanup imports 2022-09-05 20:19:08 +03:00
Ben Phelps
5a8defb478 allow weather apis to use hidden api keys 2022-09-05 20:14:14 +03:00
Ben Phelps
08afa0b747 check that content type exists before setting it 2022-09-05 10:08:02 +03:00
Ben Phelps
bad436b858 fix jellyfin widget api calls 2022-09-05 08:19:50 +03:00
Ben Phelps
7f0345a56a remove lodash, update packages 2022-09-04 21:59:20 +03:00
Ben Phelps
97bf174b78 refactor widget api design
this passes all widget API calls through the backend, with a pluggable design and reusable API handlers
2022-09-04 21:58:42 +03:00
Ben Phelps
975f79f6cc add Tautulli widget 2022-09-03 15:36:59 +03:00
Ben Phelps
e72efe7fd0 fix movies count in radarr widget 2022-09-03 14:55:16 +03:00
34 changed files with 571 additions and 358 deletions

View File

@@ -10,10 +10,10 @@
},
"dependencies": {
"@headlessui/react": "^1.6.6",
"@tailwindcss/forms": "^0.5.2",
"dockerode": "^3.3.3",
"@tailwindcss/forms": "^0.5.3",
"dockerode": "^3.3.4",
"js-yaml": "^4.1.0",
"json-rpc-2.0": "^1.3.0",
"json-rpc-2.0": "^1.4.1",
"memory-cache": "^0.2.0",
"next": "12.2.5",
"node-os-utils": "^1.3.7",
@@ -30,6 +30,6 @@
"eslint-config-next": "12.2.5",
"postcss": "^8.4.16",
"tailwindcss": "^3.1.8",
"typescript": "^4.7.4"
"typescript": "^4.8.2"
}
}

207
pnpm-lock.yaml generated
View File

@@ -2,13 +2,13 @@ lockfileVersion: 5.4
specifiers:
'@headlessui/react': ^1.6.6
'@tailwindcss/forms': ^0.5.2
'@tailwindcss/forms': ^0.5.3
autoprefixer: ^10.4.8
dockerode: ^3.3.3
dockerode: ^3.3.4
eslint: 8.22.0
eslint-config-next: 12.2.5
js-yaml: ^4.1.0
json-rpc-2.0: ^1.3.0
json-rpc-2.0: ^1.4.1
memory-cache: ^0.2.0
next: 12.2.5
node-os-utils: ^1.3.7
@@ -20,14 +20,14 @@ specifiers:
rutorrent-promise: ^2.0.0
swr: ^1.3.0
tailwindcss: ^3.1.8
typescript: ^4.7.4
typescript: ^4.8.2
dependencies:
'@headlessui/react': 1.6.6_biqbaboplfbrettd7655fr4n2y
'@tailwindcss/forms': 0.5.2_tailwindcss@3.1.8
dockerode: 3.3.3
'@tailwindcss/forms': 0.5.3_tailwindcss@3.1.8
dockerode: 3.3.4
js-yaml: 4.1.0
json-rpc-2.0: 1.3.0
json-rpc-2.0: 1.4.1
memory-cache: 0.2.0
next: 12.2.5_biqbaboplfbrettd7655fr4n2y
node-os-utils: 1.3.7
@@ -41,10 +41,10 @@ dependencies:
devDependencies:
autoprefixer: 10.4.8_postcss@8.4.16
eslint: 8.22.0
eslint-config-next: 12.2.5_4rv7y5c6xz3vfxwhbrcxxi73bq
eslint-config-next: 12.2.5_shit3uhl6a7megkzgoz6xssnfa
postcss: 8.4.16
tailwindcss: 3.1.8
typescript: 4.7.4
tailwindcss: 3.1.8_postcss@8.4.16
typescript: 4.8.2
packages:
@@ -52,7 +52,7 @@ packages:
resolution: {integrity: sha512-qZEWeccZCrHA2Au4/X05QW5CMdm4VjUDCrGq5gf1ZDcM4hRqreKrtwAn7yci9zfgAS9apvnsFXiGBHBAxZdK9A==}
engines: {node: '>=6.9.0'}
dependencies:
core-js-pure: 3.24.1
core-js-pure: 3.25.0
regenerator-runtime: 0.13.9
dev: true
@@ -63,13 +63,17 @@ packages:
regenerator-runtime: 0.13.9
dev: true
/@eslint/eslintrc/1.3.0:
resolution: {integrity: sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==}
/@balena/dockerignore/1.0.2:
resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==}
dev: false
/@eslint/eslintrc/1.3.1:
resolution: {integrity: sha512-OhSY22oQQdw3zgPOOwdoj01l/Dzl1Z+xyUP33tkSN+aqyEhymJCcPHyXt+ylW8FSe0TfRC2VG+ROQOapD0aZSQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies:
ajv: 6.12.6
debug: 4.3.4
espree: 9.3.3
espree: 9.4.0
globals: 13.17.0
ignore: 5.2.0
import-fresh: 3.3.0
@@ -265,21 +269,21 @@ packages:
tslib: 2.4.0
dev: false
/@tailwindcss/forms/0.5.2_tailwindcss@3.1.8:
resolution: {integrity: sha512-pSrFeJB6Bg1Mrg9CdQW3+hqZXAKsBrSG9MAfFLKy1pVA4Mb4W7C0k7mEhlmS2Dfo/otxrQOET7NJiJ9RrS563w==}
/@tailwindcss/forms/0.5.3_tailwindcss@3.1.8:
resolution: {integrity: sha512-y5mb86JUoiUgBjY/o6FJSFZSEttfb3Q5gllE4xoKjAAD+vBrnIhE4dViwUuow3va8mpH4s9jyUbUbrRGoRdc2Q==}
peerDependencies:
tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1'
dependencies:
mini-svg-data-uri: 1.4.4
tailwindcss: 3.1.8
tailwindcss: 3.1.8_postcss@8.4.16
dev: false
/@types/json5/0.0.29:
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
dev: true
/@typescript-eslint/parser/5.33.0_4rv7y5c6xz3vfxwhbrcxxi73bq:
resolution: {integrity: sha512-cgM5cJrWmrDV2KpvlcSkelTBASAs1mgqq+IUGKJvFxWrapHpaRy5EXPQz9YaKF3nZ8KY18ILTiVpUtbIac86/w==}
/@typescript-eslint/parser/5.36.1_shit3uhl6a7megkzgoz6xssnfa:
resolution: {integrity: sha512-/IsgNGOkBi7CuDfUbwt1eOqUXF9WGVBW9dwEe1pi+L32XrTsZIgmDFIi2RxjzsvB/8i+MIf5JIoTEH8LOZ368A==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
@@ -288,31 +292,31 @@ packages:
typescript:
optional: true
dependencies:
'@typescript-eslint/scope-manager': 5.33.0
'@typescript-eslint/types': 5.33.0
'@typescript-eslint/typescript-estree': 5.33.0_typescript@4.7.4
'@typescript-eslint/scope-manager': 5.36.1
'@typescript-eslint/types': 5.36.1
'@typescript-eslint/typescript-estree': 5.36.1_typescript@4.8.2
debug: 4.3.4
eslint: 8.22.0
typescript: 4.7.4
typescript: 4.8.2
transitivePeerDependencies:
- supports-color
dev: true
/@typescript-eslint/scope-manager/5.33.0:
resolution: {integrity: sha512-/Jta8yMNpXYpRDl8EwF/M8It2A9sFJTubDo0ATZefGXmOqlaBffEw0ZbkbQ7TNDK6q55NPHFshGBPAZvZkE8Pw==}
/@typescript-eslint/scope-manager/5.36.1:
resolution: {integrity: sha512-pGC2SH3/tXdu9IH3ItoqciD3f3RRGCh7hb9zPdN2Drsr341zgd6VbhP5OHQO/reUqihNltfPpMpTNihFMarP2w==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies:
'@typescript-eslint/types': 5.33.0
'@typescript-eslint/visitor-keys': 5.33.0
'@typescript-eslint/types': 5.36.1
'@typescript-eslint/visitor-keys': 5.36.1
dev: true
/@typescript-eslint/types/5.33.0:
resolution: {integrity: sha512-nIMt96JngB4MYFYXpZ/3ZNU4GWPNdBbcB5w2rDOCpXOVUkhtNlG2mmm8uXhubhidRZdwMaMBap7Uk8SZMU/ppw==}
/@typescript-eslint/types/5.36.1:
resolution: {integrity: sha512-jd93ShpsIk1KgBTx9E+hCSEuLCUFwi9V/urhjOWnOaksGZFbTOxAT47OH2d4NLJnLhkVD+wDbB48BuaycZPLBg==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dev: true
/@typescript-eslint/typescript-estree/5.33.0_typescript@4.7.4:
resolution: {integrity: sha512-tqq3MRLlggkJKJUrzM6wltk8NckKyyorCSGMq4eVkyL5sDYzJJcMgZATqmF8fLdsWrW7OjjIZ1m9v81vKcaqwQ==}
/@typescript-eslint/typescript-estree/5.36.1_typescript@4.8.2:
resolution: {integrity: sha512-ih7V52zvHdiX6WcPjsOdmADhYMDN15SylWRZrT2OMy80wzKbc79n8wFW0xpWpU0x3VpBz/oDgTm2xwDAnFTl+g==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
typescript: '*'
@@ -320,23 +324,23 @@ packages:
typescript:
optional: true
dependencies:
'@typescript-eslint/types': 5.33.0
'@typescript-eslint/visitor-keys': 5.33.0
'@typescript-eslint/types': 5.36.1
'@typescript-eslint/visitor-keys': 5.36.1
debug: 4.3.4
globby: 11.1.0
is-glob: 4.0.3
semver: 7.3.7
tsutils: 3.21.0_typescript@4.7.4
typescript: 4.7.4
tsutils: 3.21.0_typescript@4.8.2
typescript: 4.8.2
transitivePeerDependencies:
- supports-color
dev: true
/@typescript-eslint/visitor-keys/5.33.0:
resolution: {integrity: sha512-/XsqCzD4t+Y9p5wd9HZiptuGKBlaZO5showwqODii5C0nZawxWLF+Q6k5wYHBrQv96h6GYKyqqMHCSTqta8Kiw==}
/@typescript-eslint/visitor-keys/5.36.1:
resolution: {integrity: sha512-ojB9aRyRFzVMN3b5joSYni6FAS10BBSCAfKJhjJAV08t/a95aM6tAhz+O1jF+EtgxktuSO3wJysp2R+Def/IWQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies:
'@typescript-eslint/types': 5.33.0
'@typescript-eslint/types': 5.36.1
eslint-visitor-keys: 3.3.0
dev: true
@@ -418,7 +422,7 @@ packages:
dependencies:
call-bind: 1.0.2
define-properties: 1.1.4
es-abstract: 1.20.1
es-abstract: 1.20.2
get-intrinsic: 1.1.2
is-string: 1.0.7
dev: true
@@ -434,7 +438,7 @@ packages:
dependencies:
call-bind: 1.0.2
define-properties: 1.1.4
es-abstract: 1.20.1
es-abstract: 1.20.2
es-shim-unscopables: 1.0.0
dev: true
@@ -444,7 +448,7 @@ packages:
dependencies:
call-bind: 1.0.2
define-properties: 1.1.4
es-abstract: 1.20.1
es-abstract: 1.20.2
es-shim-unscopables: 1.0.0
dev: true
@@ -470,7 +474,7 @@ packages:
postcss: ^8.1.0
dependencies:
browserslist: 4.21.3
caniuse-lite: 1.0.30001375
caniuse-lite: 1.0.30001388
fraction.js: 4.2.0
normalize-range: 0.1.2
picocolors: 1.0.0
@@ -531,10 +535,10 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
dependencies:
caniuse-lite: 1.0.30001375
electron-to-chromium: 1.4.219
caniuse-lite: 1.0.30001388
electron-to-chromium: 1.4.241
node-releases: 2.0.6
update-browserslist-db: 1.0.5_browserslist@4.21.3
update-browserslist-db: 1.0.7_browserslist@4.21.3
dev: true
/buffer/5.7.1:
@@ -571,8 +575,8 @@ packages:
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
engines: {node: '>= 6'}
/caniuse-lite/1.0.30001375:
resolution: {integrity: sha512-kWIMkNzLYxSvnjy0hL8w1NOaWNr2rn39RTAVyIwcw8juu60bZDWiF1/loOYANzjtJmy6qPgNmn38ro5Pygagdw==}
/caniuse-lite/1.0.30001388:
resolution: {integrity: sha512-znVbq4OUjqgLxMxoNX2ZeeLR0d7lcDiE5uJ4eUiWdml1J1EkxbnQq6opT9jb9SMfJxB0XA16/ziHwni4u1I3GQ==}
/chalk/4.1.2:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
@@ -621,8 +625,8 @@ packages:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
dev: true
/core-js-pure/3.24.1:
resolution: {integrity: sha512-r1nJk41QLLPyozHUUPmILCEMtMw24NG4oWK6RbsDdjzQgg9ZvrUsPBj1MnG0wXXp1DCDU6j+wUvEmBSrtRbLXg==}
/core-js-pure/3.25.0:
resolution: {integrity: sha512-IeHpLwk3uoci37yoI2Laty59+YqH9x5uR65/yiA0ARAJrTrN4YU0rmauLWfvqOuk77SlNJXj2rM6oT/dBD87+A==}
requiresBuild: true
dev: true
@@ -734,8 +738,8 @@ packages:
/dlv/1.1.3:
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
/docker-modem/3.0.5:
resolution: {integrity: sha512-x1E6jxWdtoK3+ifAUWj4w5egPdTDGBpesSCErm+aKET5BnnEOvDtTP6GxcnMB1zZiv2iQ0qJZvJie+1wfIRg6Q==}
/docker-modem/3.0.6:
resolution: {integrity: sha512-h0Ow21gclbYsZ3mkHDfsYNDqtRhXS8fXr51bU0qr1dxgTMJj0XufbzX+jhNOvA8KuEEzn6JbvLVhXyv+fny9Uw==}
engines: {node: '>= 8.0'}
dependencies:
debug: 4.3.4
@@ -746,11 +750,12 @@ packages:
- supports-color
dev: false
/dockerode/3.3.3:
resolution: {integrity: sha512-lvKV6/NGf2/CYLt5V4c0fd6Fl9XZSCo1Z2HBT9ioKrKLMB2o+gA62Uza8RROpzGvYv57KJx2dKu+ZwSpB//OIA==}
/dockerode/3.3.4:
resolution: {integrity: sha512-3EUwuXnCU+RUlQEheDjmBE0B7q66PV9Rw5NiH1sXwINq0M9c5ERP9fxgkw36ZHOtzf4AGEEYySnkx/sACC9EgQ==}
engines: {node: '>= 8.0'}
dependencies:
docker-modem: 3.0.5
'@balena/dockerignore': 1.0.2
docker-modem: 3.0.6
tar-fs: 2.0.1
transitivePeerDependencies:
- supports-color
@@ -770,8 +775,8 @@ packages:
esutils: 2.0.3
dev: true
/electron-to-chromium/1.4.219:
resolution: {integrity: sha512-zoQJsXOUw0ZA0YxbjkmzBumAJRtr6je5JySuL/bAoFs0DuLiLJ+5FzRF7/ZayihxR2QcewlRZVm5QZdUhwjOgA==}
/electron-to-chromium/1.4.241:
resolution: {integrity: sha512-e7Wsh4ilaioBZ5bMm6+F4V5c11dh56/5Jwz7Hl5Tu1J7cnB+Pqx5qIF2iC7HPpfyQMqGSvvLP5bBAIDd2gAtGw==}
dev: true
/emoji-regex/9.2.2:
@@ -784,8 +789,8 @@ packages:
once: 1.4.0
dev: false
/es-abstract/1.20.1:
resolution: {integrity: sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA==}
/es-abstract/1.20.2:
resolution: {integrity: sha512-XxXQuVNrySBNlEkTYJoDNFe5+s2yIOpzq80sUHEdPdQr0S5nTLz4ZPPPswNIpKseDDUS5yghX1gfLIHQZ1iNuQ==}
engines: {node: '>= 0.4'}
dependencies:
call-bind: 1.0.2
@@ -806,7 +811,7 @@ packages:
is-weakref: 1.0.2
object-inspect: 1.12.2
object-keys: 1.1.1
object.assign: 4.1.3
object.assign: 4.1.4
regexp.prototype.flags: 1.4.3
string.prototype.trimend: 1.0.5
string.prototype.trimstart: 1.0.5
@@ -838,7 +843,7 @@ packages:
engines: {node: '>=10'}
dev: true
/eslint-config-next/12.2.5_4rv7y5c6xz3vfxwhbrcxxi73bq:
/eslint-config-next/12.2.5_shit3uhl6a7megkzgoz6xssnfa:
resolution: {integrity: sha512-SOowilkqPzW6DxKp3a3SYlrfPi5Ajs9MIzp9gVfUDxxH9QFM5ElkR1hX5m/iICJuvCbWgQqFBiA3mCMozluniw==}
peerDependencies:
eslint: ^7.23.0 || ^8.0.0
@@ -849,15 +854,15 @@ packages:
dependencies:
'@next/eslint-plugin-next': 12.2.5
'@rushstack/eslint-patch': 1.1.4
'@typescript-eslint/parser': 5.33.0_4rv7y5c6xz3vfxwhbrcxxi73bq
'@typescript-eslint/parser': 5.36.1_shit3uhl6a7megkzgoz6xssnfa
eslint: 8.22.0
eslint-import-resolver-node: 0.3.6
eslint-import-resolver-typescript: 2.7.1_2iahngt3u2tkbdlu6s4gkur3pu
eslint-plugin-import: 2.26.0_42jdfezp7lcuhr3fexihng3k3a
eslint-plugin-import: 2.26.0_oqagwj4pgbbpoeodu52b4a34xi
eslint-plugin-jsx-a11y: 6.6.1_eslint@8.22.0
eslint-plugin-react: 7.30.1_eslint@8.22.0
eslint-plugin-react: 7.31.5_eslint@8.22.0
eslint-plugin-react-hooks: 4.6.0_eslint@8.22.0
typescript: 4.7.4
typescript: 4.8.2
transitivePeerDependencies:
- eslint-import-resolver-webpack
- supports-color
@@ -881,7 +886,7 @@ packages:
dependencies:
debug: 4.3.4
eslint: 8.22.0
eslint-plugin-import: 2.26.0_42jdfezp7lcuhr3fexihng3k3a
eslint-plugin-import: 2.26.0_oqagwj4pgbbpoeodu52b4a34xi
glob: 7.2.3
is-glob: 4.0.3
resolve: 1.22.1
@@ -890,7 +895,7 @@ packages:
- supports-color
dev: true
/eslint-module-utils/2.7.4_2znkvn5grarov6xvamxzzjpwtu:
/eslint-module-utils/2.7.4_56k6siz3uc4k5fooi777xiw6ie:
resolution: {integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==}
engines: {node: '>=4'}
peerDependencies:
@@ -911,7 +916,7 @@ packages:
eslint-import-resolver-webpack:
optional: true
dependencies:
'@typescript-eslint/parser': 5.33.0_4rv7y5c6xz3vfxwhbrcxxi73bq
'@typescript-eslint/parser': 5.36.1_shit3uhl6a7megkzgoz6xssnfa
debug: 3.2.7
eslint: 8.22.0
eslint-import-resolver-node: 0.3.6
@@ -920,7 +925,7 @@ packages:
- supports-color
dev: true
/eslint-plugin-import/2.26.0_42jdfezp7lcuhr3fexihng3k3a:
/eslint-plugin-import/2.26.0_oqagwj4pgbbpoeodu52b4a34xi:
resolution: {integrity: sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==}
engines: {node: '>=4'}
peerDependencies:
@@ -930,14 +935,14 @@ packages:
'@typescript-eslint/parser':
optional: true
dependencies:
'@typescript-eslint/parser': 5.33.0_4rv7y5c6xz3vfxwhbrcxxi73bq
'@typescript-eslint/parser': 5.36.1_shit3uhl6a7megkzgoz6xssnfa
array-includes: 3.1.5
array.prototype.flat: 1.3.0
debug: 2.6.9
doctrine: 2.1.0
eslint: 8.22.0
eslint-import-resolver-node: 0.3.6
eslint-module-utils: 2.7.4_2znkvn5grarov6xvamxzzjpwtu
eslint-module-utils: 2.7.4_56k6siz3uc4k5fooi777xiw6ie
has: 1.0.3
is-core-module: 2.10.0
is-glob: 4.0.3
@@ -982,8 +987,8 @@ packages:
eslint: 8.22.0
dev: true
/eslint-plugin-react/7.30.1_eslint@8.22.0:
resolution: {integrity: sha512-NbEvI9jtqO46yJA3wcRF9Mo0lF9T/jhdHqhCHXiXtD+Zcb98812wvokjWpU7Q4QH5edo6dmqrukxVvWWXHlsUg==}
/eslint-plugin-react/7.31.5_eslint@8.22.0:
resolution: {integrity: sha512-y7472VAcqns17rsQUk6tQCnqBi+boYjGdYarX022719+wGd1T4U1fOYJ2T2Trd3Od2q5M92e42zJ2uZOGmWamA==}
engines: {node: '>=4'}
peerDependencies:
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8
@@ -1038,7 +1043,7 @@ packages:
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
hasBin: true
dependencies:
'@eslint/eslintrc': 1.3.0
'@eslint/eslintrc': 1.3.1
'@humanwhocodes/config-array': 0.10.4
'@humanwhocodes/gitignore-to-minimatch': 1.0.2
ajv: 6.12.6
@@ -1050,7 +1055,7 @@ packages:
eslint-scope: 7.1.1
eslint-utils: 3.0.0_eslint@8.22.0
eslint-visitor-keys: 3.3.0
espree: 9.3.3
espree: 9.4.0
esquery: 1.4.0
esutils: 2.0.3
fast-deep-equal: 3.1.3
@@ -1081,8 +1086,8 @@ packages:
- supports-color
dev: true
/espree/9.3.3:
resolution: {integrity: sha512-ORs1Rt/uQTqUKjDdGCyrtYxbazf5umATSf/K4qxjmZHORR6HJk+2s/2Pqe+Kk49HHINC/xNIrGfgh8sZcll0ng==}
/espree/9.4.0:
resolution: {integrity: sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies:
acorn: 8.8.0
@@ -1166,12 +1171,12 @@ packages:
resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==}
engines: {node: ^10.12.0 || >=12.0.0}
dependencies:
flatted: 3.2.6
flatted: 3.2.7
rimraf: 3.0.2
dev: true
/flatted/3.2.6:
resolution: {integrity: sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ==}
/flatted/3.2.7:
resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==}
dev: true
/form-data/3.0.1:
@@ -1211,7 +1216,7 @@ packages:
dependencies:
call-bind: 1.0.2
define-properties: 1.1.4
es-abstract: 1.20.1
es-abstract: 1.20.2
functions-have-names: 1.2.3
dev: true
@@ -1498,8 +1503,8 @@ packages:
dependencies:
argparse: 2.0.1
/json-rpc-2.0/1.3.0:
resolution: {integrity: sha512-pA85D9LG4B+b8njdb8DzktltNfGPcxGJJydEg6jRqePfwhK008lRdbX7bco6aokfw9oDFw7fEe+LRxxXwP11HA==}
/json-rpc-2.0/1.4.1:
resolution: {integrity: sha512-OX1NJhpIfuK4GjDnJ/gKtZy1HOYo0l4eL0a4rb0rNeQheX1xlyQ9+JMmPzs/sFNthpS/TXKPWlGo09X7B5l81A==}
dev: false
/json-schema-traverse/0.4.1:
@@ -1522,7 +1527,7 @@ packages:
engines: {node: '>=4.0'}
dependencies:
array-includes: 3.1.5
object.assign: 4.1.3
object.assign: 4.1.4
dev: true
/language-subtag-registry/0.3.22:
@@ -1661,7 +1666,7 @@ packages:
dependencies:
'@next/env': 12.2.5
'@swc/helpers': 0.4.3
caniuse-lite: 1.0.30001375
caniuse-lite: 1.0.30001388
postcss: 8.4.14
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
@@ -1733,8 +1738,8 @@ packages:
engines: {node: '>= 0.4'}
dev: true
/object.assign/4.1.3:
resolution: {integrity: sha512-ZFJnX3zltyjcYJL0RoCJuzb+11zWGyaDbjgxZbdV7rFEcHQuYxrZqhow67aA7xpes6LhojyFDaBKAFfogQrikA==}
/object.assign/4.1.4:
resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==}
engines: {node: '>= 0.4'}
dependencies:
call-bind: 1.0.2
@@ -1749,7 +1754,7 @@ packages:
dependencies:
call-bind: 1.0.2
define-properties: 1.1.4
es-abstract: 1.20.1
es-abstract: 1.20.2
dev: true
/object.fromentries/2.0.5:
@@ -1758,14 +1763,14 @@ packages:
dependencies:
call-bind: 1.0.2
define-properties: 1.1.4
es-abstract: 1.20.1
es-abstract: 1.20.2
dev: true
/object.hasown/1.1.1:
resolution: {integrity: sha512-LYLe4tivNQzq4JdaWW6WO3HMZZJWzkkH8fnI6EebWl0VZth2wL2Lovm74ep2/gZzlaTdV62JZHEqHQ2yVn8Q/A==}
dependencies:
define-properties: 1.1.4
es-abstract: 1.20.1
es-abstract: 1.20.2
dev: true
/object.values/1.1.5:
@@ -1774,7 +1779,7 @@ packages:
dependencies:
call-bind: 1.0.2
define-properties: 1.1.4
es-abstract: 1.20.1
es-abstract: 1.20.2
dev: true
/once/1.4.0:
@@ -2163,7 +2168,7 @@ packages:
dependencies:
call-bind: 1.0.2
define-properties: 1.1.4
es-abstract: 1.20.1
es-abstract: 1.20.2
get-intrinsic: 1.1.2
has-symbols: 1.0.3
internal-slot: 1.0.3
@@ -2176,7 +2181,7 @@ packages:
dependencies:
call-bind: 1.0.2
define-properties: 1.1.4
es-abstract: 1.20.1
es-abstract: 1.20.2
dev: true
/string.prototype.trimstart/1.0.5:
@@ -2184,7 +2189,7 @@ packages:
dependencies:
call-bind: 1.0.2
define-properties: 1.1.4
es-abstract: 1.20.1
es-abstract: 1.20.2
dev: true
/string_decoder/1.3.0:
@@ -2245,10 +2250,12 @@ packages:
react: 18.2.0
dev: false
/tailwindcss/3.1.8:
/tailwindcss/3.1.8_postcss@8.4.16:
resolution: {integrity: sha512-YSneUCZSFDYMwk+TGq8qYFdCA3yfBRdBlS7txSq0LUmzyeqRe3a8fBQzbz9M3WS/iFT4BNf/nmw9mEzrnSaC0g==}
engines: {node: '>=12.13.0'}
hasBin: true
peerDependencies:
postcss: ^8.0.9
dependencies:
arg: 5.0.2
chokidar: 3.5.3
@@ -2331,14 +2338,14 @@ packages:
resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==}
dev: false
/tsutils/3.21.0_typescript@4.7.4:
/tsutils/3.21.0_typescript@4.8.2:
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
engines: {node: '>= 6'}
peerDependencies:
typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta'
dependencies:
tslib: 1.14.1
typescript: 4.7.4
typescript: 4.8.2
dev: true
/tweetnacl/0.14.5:
@@ -2357,8 +2364,8 @@ packages:
engines: {node: '>=10'}
dev: true
/typescript/4.7.4:
resolution: {integrity: sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==}
/typescript/4.8.2:
resolution: {integrity: sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw==}
engines: {node: '>=4.2.0'}
hasBin: true
dev: true
@@ -2377,8 +2384,8 @@ packages:
engines: {node: '>= 0.8'}
dev: false
/update-browserslist-db/1.0.5_browserslist@4.21.3:
resolution: {integrity: sha512-dteFFpCyvuDdr9S/ff1ISkKt/9YZxKjI9WlRR99c180GaztJtRa/fn18FdxGVKVsnPY7/a/FDN68mcvUmP4U7Q==}
/update-browserslist-db/1.0.7_browserslist@4.21.3:
resolution: {integrity: sha512-iN/XYesmZ2RmmWAiI4Z5rq0YqSiv0brj9Ce9CfhNE4xIW2h+MFxcgkxIzZ+ShkFPUkjU3gQ+3oypadD3RAMtrg==}
hasBin: true
peerDependencies:
browserslist: '>= 4.21.0'

View File

@@ -12,6 +12,7 @@ import Speedtest from "./widgets/service/speedtest";
import Traefik from "./widgets/service/traefik";
import Jellyseerr from "./widgets/service/jellyseerr";
import Npm from "./widgets/service/npm";
import Tautulli from "./widgets/service/tautulli";
const widgetMappings = {
docker: Docker,
@@ -28,6 +29,7 @@ const widgetMappings = {
traefik: Traefik,
jellyseerr: Jellyseerr,
npm: Npm,
tautulli: Tautulli,
};
export default function Widget({ service }) {

View File

@@ -1,24 +1,24 @@
import useSWR from "swr";
import { calculateCPUPercent, formatBytes } from "utils/stats-helpers";
import Widget from "../widget";
import Block from "../block";
import { calculateCPUPercent, formatBytes } from "utils/stats-helpers";
export default function Docker({ service }) {
const config = service.widget;
const { data: statusData, error: statusError } = useSWR(
`/api/docker/status/${config.container}/${config.server || ""}`,
{
refreshInterval: 1500,
refreshInterval: 5000,
}
);
const { data: statsData, error: statsError } = useSWR(
`/api/docker/stats/${config.container}/${config.server || ""}`,
{
refreshInterval: 1500,
refreshInterval: 5000,
}
);

View File

@@ -3,17 +3,12 @@ import useSWR from "swr";
import Widget from "../widget";
import Block from "../block";
import { formatApiUrl } from "utils/api-helpers";
export default function Emby({ service, title = "Emby" }) {
const config = service.widget;
function buildApiUrl(endpoint) {
const { url, key } = config;
return `${url}/emby/${endpoint}?api_key=${key}`;
}
const { data: sessionsData, error: sessionsError } = useSWR(buildApiUrl(`Sessions`), {
refreshInterval: 1000,
});
const { data: sessionsData, error: sessionsError } = useSWR(formatApiUrl(config, "Sessions"));
if (sessionsError) {
return <Widget error={`${title} API Error`} />;

View File

@@ -3,49 +3,32 @@ import useSWR from "swr";
import Widget from "../widget";
import Block from "../block";
import { formatApiUrl } from "utils/api-helpers";
export default function Jellyseerr({ service }) {
const config = service.widget;
const config = service.widget;
function buildApiUrl(endpoint) {
const { url } = config;
const reqUrl = new URL(`/api/v1/${endpoint}`, url);
return `/api/proxy?url=${encodeURIComponent(reqUrl)}`;
}
const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, `request/count`));
const fetcher = async (url) => {
const res = await fetch(url, {
method: "GET",
withCredentials: true,
credentials: "include",
headers: {
"X-Api-Key": `${config.key}`,
"Content-Type": "application/json"
}
});
return await res.json();
};
const { data: statsData, error: statsError } = useSWR(buildApiUrl(`request/count`), fetcher);
if (statsError) {
return <Widget error="Jellyseerr API Error" />;
}
if (!statsData) {
return (
<Widget>
<Block label="Pending" />
<Block label="Approved" />
<Block label="Available" />
</Widget>
);
}
if (statsError) {
return <Widget error="Jellyseerr API Error" />;
}
if (!statsData) {
return (
<Widget>
<Block label="Pending" value={statsData.pending} />
<Block label="Approved" value={statsData.approved} />
<Block label="Available" value={statsData.available} />
</Widget>
<Widget>
<Block label="Pending" />
<Block label="Approved" />
<Block label="Available" />
</Widget>
);
}
return (
<Widget>
<Block label="Pending" value={statsData.pending} />
<Block label="Approved" value={statsData.approved} />
<Block label="Available" value={statsData.available} />
</Widget>
);
}

View File

@@ -3,39 +3,12 @@ import useSWR from "swr";
import Widget from "../widget";
import Block from "../block";
import { formatApiUrl } from "utils/api-helpers";
export default function Npm({ service }) {
const config = service.widget;
const { url } = config;
const fetcher = async (reqUrl) => {
const { url, username, password } = config;
const loginUrl = `${url}/api/tokens`;
const body = { identity: username, secret: password };
const res = await fetch(loginUrl, {
method: "POST",
body: JSON.stringify(body),
headers: {
"Content-Type": "application/json",
},
})
.then((response) => response.json())
.then(
async (data) =>
await fetch(reqUrl, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + data.token,
},
})
);
return res.json();
};
const { data: infoData, error: infoError } = useSWR(`${url}/api/nginx/proxy-hosts`, fetcher);
console.log(infoData);
const { data: infoData, error: infoError } = useSWR(formatApiUrl(config, "nginx/proxy-hosts"));
if (infoError) {
return <Widget error="NGINX Proxy Manager API Error" />;

View File

@@ -1,44 +1,15 @@
import useSWR from "swr";
import { JSONRPCClient } from "json-rpc-2.0";
import { formatBytes } from "utils/stats-helpers";
import Widget from "../widget";
import Block from "../block";
import { formatApiUrl } from "utils/api-helpers";
import { formatBytes } from "utils/stats-helpers";
export default function Nzbget({ service }) {
const config = service.widget;
const constructedUrl = new URL(config.url);
constructedUrl.pathname = "jsonrpc";
const client = new JSONRPCClient((jsonRPCRequest) =>
fetch(constructedUrl.toString(), {
method: "POST",
headers: {
"content-type": "application/json",
authorization: `Basic ${btoa(`${config.username}:${config.password}`)}`,
},
body: JSON.stringify(jsonRPCRequest),
}).then(async (response) => {
if (response.status === 200) {
const jsonRPCResponse = await response.json();
return client.receive(jsonRPCResponse);
} else if (jsonRPCRequest.id !== undefined) {
return Promise.reject(new Error(response.statusText));
}
})
);
const { data: statusData, error: statusError } = useSWR(
"status",
(resource) => {
return client.request(resource).then((response) => response);
},
{
refreshInterval: 1000,
}
);
const { data: statusData, error: statusError } = useSWR(formatApiUrl(config, "status"));
if (statusError) {
return <Widget error="Nzbget API Error" />;

View File

@@ -3,27 +3,12 @@ import useSWR from "swr";
import Widget from "../widget";
import Block from "../block";
import { formatApiUrl } from "utils/api-helpers";
export default function Ombi({ service }) {
const config = service.widget;
function buildApiUrl(endpoint) {
const { url } = config;
return `${url}/api/v1/${endpoint}`;
}
const fetcher = (url) => {
return fetch(url, {
method: "GET",
withCredentials: true,
credentials: "include",
headers: {
ApiKey: `${config.key}`,
"Content-Type": "application/json",
},
}).then((res) => res.json());
};
const { data: statsData, error: statsError } = useSWR(buildApiUrl(`Request/count`), fetcher);
const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, `Request/count`));
if (statsError) {
return <Widget error="Ombi API Error" />;

View File

@@ -3,21 +3,12 @@ import useSWR from "swr";
import Widget from "../widget";
import Block from "../block";
import { formatApiUrl } from "utils/api-helpers";
export default function Pihole({ service }) {
const config = service.widget;
function buildApiUrl(endpoint) {
const { url, proxy } = config;
if (proxy) {
const fullUrl = `${url}/admin/${endpoint}`;
return "/api/proxy?url=" + encodeURIComponent(fullUrl);
}
return `${url}/admin/${endpoint}`;
}
const { data: piholeData, error: piholeError } = useSWR(buildApiUrl("api.php"));
const { data: piholeData, error: piholeError } = useSWR(formatApiUrl(config, "api.php"));
if (piholeError) {
return <Widget error="PiHole API Error" />;

View File

@@ -3,29 +3,12 @@ import useSWR from "swr";
import Widget from "../widget";
import Block from "../block";
import { formatApiUrl } from "utils/api-helpers";
export default function Portainer({ service }) {
const config = service.widget;
function buildApiUrl(endpoint) {
const { url, env } = config;
const reqUrl = new URL(`/api/endpoints/${env}/${endpoint}`, url);
return `/api/proxy?url=${encodeURIComponent(reqUrl)}`;
}
const fetcher = async (url) => {
const res = await fetch(url, {
method: "GET",
withCredentials: true,
credentials: "include",
headers: {
"X-API-Key": `${config.key}`,
"Content-Type": "application/json",
},
});
return await res.json();
};
const { data: containersData, error: containersError } = useSWR(buildApiUrl(`docker/containers/json?all=1`), fetcher);
const { data: containersData, error: containersError } = useSWR(formatApiUrl(config, `docker/containers/json?all=1`));
if (containersError) {
return <Widget error="Portainer API Error" />;

View File

@@ -3,16 +3,13 @@ import useSWR from "swr";
import Widget from "../widget";
import Block from "../block";
import { formatApiUrl } from "utils/api-helpers";
export default function Radarr({ service }) {
const config = service.widget;
function buildApiUrl(endpoint) {
const { url, key } = config;
return `${url}/api/v3/${endpoint}?apikey=${key}`;
}
const { data: moviesData, error: moviesError } = useSWR(buildApiUrl("movie"));
const { data: queuedData, error: queuedError } = useSWR(buildApiUrl("queue/status"));
const { data: moviesData, error: moviesError } = useSWR(formatApiUrl(config, "movie"));
const { data: queuedData, error: queuedError } = useSWR(formatApiUrl(config, "queue/status"));
if (moviesError || queuedError) {
return <Widget error="Radarr API Error" />;
@@ -35,7 +32,7 @@ export default function Radarr({ service }) {
<Widget>
<Block label="Wanted" value={wanted.length} />
<Block label="Queued" value={queuedData.totalCount} />
<Block label="Movies" value={moviesData.length} />
<Block label="Movies" value={have.length} />
</Widget>
);
}

View File

@@ -1,32 +1,15 @@
import useSWR from "swr";
import RuTorrent from "rutorrent-promise";
import { formatBytes } from "utils/stats-helpers";
import Widget from "../widget";
import Block from "../block";
import { formatApiUrl } from "utils/api-helpers";
import { formatBytes } from "utils/stats-helpers";
export default function Rutorrent({ service }) {
const config = service.widget;
function buildApiUrl() {
const { url, username, password } = config;
const options = {
url: `${url}/plugins/httprpc/action.php`,
};
if (username && password) {
options.username = username;
options.password = password;
}
const params = new URLSearchParams(options);
return `/api/widgets/rutorrent?${params.toString()}`;
}
const { data: statusData, error: statusError } = useSWR(buildApiUrl());
const { data: statusData, error: statusError } = useSWR(formatApiUrl(config));
if (statusError) {
return <Widget error="Nzbget API Error" />;

View File

@@ -3,17 +3,14 @@ import useSWR from "swr";
import Widget from "../widget";
import Block from "../block";
import { formatApiUrl } from "utils/api-helpers";
export default function Sonarr({ service }) {
const config = service.widget;
function buildApiUrl(endpoint) {
const { url, key } = config;
return `${url}/api/v3/${endpoint}?apikey=${key}`;
}
const { data: wantedData, error: wantedError } = useSWR(buildApiUrl("wanted/missing"));
const { data: queuedData, error: queuedError } = useSWR(buildApiUrl("queue"));
const { data: seriesData, error: seriesError } = useSWR(buildApiUrl("series"));
const { data: wantedData, error: wantedError } = useSWR(formatApiUrl(config, "wanted/missing"));
const { data: queuedData, error: queuedError } = useSWR(formatApiUrl(config, "queue"));
const { data: seriesData, error: seriesError } = useSWR(formatApiUrl(config, "series"));
if (wantedError || queuedError || seriesError) {
return <Widget error="Sonar API Error" />;

View File

@@ -4,16 +4,12 @@ import Widget from "../widget";
import Block from "../block";
import { formatBits } from "utils/stats-helpers";
import { formatApiUrl } from "utils/api-helpers";
export default function Speedtest({ service }) {
const config = service.widget;
function buildApiUrl(endpoint) {
const { url } = config;
return `${url}/api/${endpoint}`;
}
const { data: speedtestData, error: speedtestError } = useSWR(buildApiUrl("speedtest/latest"));
const { data: speedtestData, error: speedtestError } = useSWR(formatApiUrl(config, "speedtest/latest"));
if (speedtestError) {
return <Widget error="Speedtest API Error" />;
@@ -31,8 +27,8 @@ export default function Speedtest({ service }) {
return (
<Widget>
<Block label="Download" value={`${formatBits(speedtestData.data.download * 1024 * 1024)}ps`} />
<Block label="Upload" value={`${formatBits(speedtestData.data.upload * 1024 * 1024)}ps`} />
<Block label="Download" value={`${formatBits(speedtestData.data.download * 1024 * 1024, 0)}ps`} />
<Block label="Upload" value={`${formatBits(speedtestData.data.upload * 1024 * 1024, 0)}ps`} />
<Block label="Ping" value={`${speedtestData.data.ping} ms`} />
</Widget>
);

View File

@@ -0,0 +1,37 @@
import useSWR from "swr";
import Widget from "../widget";
import Block from "../block";
import { formatApiUrl } from "utils/api-helpers";
export default function Tautulli({ service }) {
const config = service.widget;
const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, "get_activity"));
if (statsError) {
return <Widget error="Tautulli API Error" />;
}
if (!statsData) {
return (
<Widget>
<Block label="Playing" />
<Block label="Transcoding" />
<Block label="Bitrate" />
</Widget>
);
}
const data = statsData.response.data;
return (
<Widget>
<Block label="Playing" value={data.stream_count} />
<Block label="Transcoding" value={data.stream_count_transcode} />
{/* We divide by 1000 here because thats how Tautulli reports it on its own dashboard */}
<Block label="Bitrate" value={`${Math.round((data.total_bandwidth / 1000) * 100) / 100} Mbps`} />
</Widget>
);
}

View File

@@ -3,16 +3,12 @@ import useSWR from "swr";
import Widget from "../widget";
import Block from "../block";
import { formatApiUrl } from "utils/api-helpers";
export default function Traefik({ service }) {
const config = service.widget;
function buildApiUrl(endpoint) {
const { url } = config;
const fullUrl = `${url}/api/${endpoint}`;
return `/api/proxy?url=${encodeURIComponent(fullUrl)}`;
}
const { data: traefikData, error: traefikError } = useSWR(buildApiUrl("overview"));
const { data: traefikData, error: traefikError } = useSWR(formatApiUrl(config, "overview"));
if (traefikError) {
return <Widget error="Traefik API Error" />;

View File

@@ -4,9 +4,7 @@ import { BiError } from "react-icons/bi";
import Icon from "./icon";
export default function OpenWeatherMap({ options }) {
const { data, error } = useSWR(
`/api/widgets/openweathermap?lat=${options.latitude}&lon=${options.longitude}&apiKey=${options.apiKey}&duration=${options.cache}&units=${options.units}`
);
const { data, error } = useSWR(`/api/widgets/openweathermap?${new URLSearchParams(options).toString()}`);
if (error || data?.cod == 401) {
return (

View File

@@ -4,9 +4,8 @@ import { BiError } from "react-icons/bi";
import Icon from "./icon";
export default function WeatherApi({ options }) {
const { data, error } = useSWR(
`/api/widgets/weather?lat=${options.latitude}&lon=${options.longitude}&apiKey=${options.apiKey}&duration=${options.cache}`
);
console.log(options);
const { data, error } = useSWR(`/api/widgets/weather?${new URLSearchParams(options).toString()}`);
if (error) {
return (

View File

@@ -15,10 +15,23 @@ export default async function handler(req, res) {
return {
name: Object.keys(group)[0],
services: group[Object.keys(group)[0]].map((entries) => {
return {
const { widget, ...service } = entries[Object.keys(entries)[0]];
let res = {
name: Object.keys(entries)[0],
...entries[Object.keys(entries)[0]],
...service,
};
if (widget) {
const { type } = widget;
res.widget = {
type: type,
service_group: Object.keys(group)[0],
service_name: Object.keys(entries)[0],
};
}
return res;
}),
};
});

View File

@@ -0,0 +1,37 @@
import genericProxyHandler from "utils/proxies/generic";
import credentialedProxyHandler from "utils/proxies/credentialed";
import rutorrentProxyHandler from "utils/proxies/rutorrent";
import nzbgetProxyHandler from "utils/proxies/nzbget";
import npmProxyHandler from "utils/proxies/npm";
const serviceProxyHandlers = {
// uses query param auth
emby: genericProxyHandler,
jellyfin: genericProxyHandler,
pihole: genericProxyHandler,
radarr: genericProxyHandler,
sonarr: genericProxyHandler,
speedtest: genericProxyHandler,
tautulli: genericProxyHandler,
traefik: genericProxyHandler,
// uses X-API-Key header auth
portainer: credentialedProxyHandler,
jellyseerr: credentialedProxyHandler,
ombi: credentialedProxyHandler,
// super specific handlers
rutorrent: rutorrentProxyHandler,
nzbget: nzbgetProxyHandler,
npm: npmProxyHandler,
};
export default async function handler(req, res) {
const { type } = req.query;
const serviceProxyHandler = serviceProxyHandlers[type];
if (serviceProxyHandler) {
return serviceProxyHandler(req, res);
}
res.status(403).json({ error: "Unkown proxy service type" });
}

View File

@@ -1,9 +1,28 @@
import cachedFetch from "utils/cached-fetch";
import { getSettings } from "utils/config";
export default async function handler(req, res) {
const { lat, lon, apiKey, duration, units } = req.query;
const { latitude, longitude, units, provider, cache } = req.query;
let { apiKey } = req.query;
const api_url = `https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&appid=${apiKey}&units=${units}`;
if (!apiKey && !provider) {
return res.status(400).json({ error: "Missing API key or provider" });
}
res.send(await cachedFetch(api_url, duration));
if (!apiKey && provider !== "openweathermap") {
return res.status(400).json({ error: "Invalid provider for endpoint" });
}
if (!apiKey && provider) {
const settings = await getSettings();
apiKey = settings?.providers?.openweathermap;
}
if (!apiKey) {
return res.status(400).json({ error: "Missing API key" });
}
const api_url = `https://api.openweathermap.org/data/2.5/weather?lat=${latitude}&lon=${longitude}&appid=${apiKey}&units=${units}`;
res.send(await cachedFetch(api_url, cache));
}

View File

@@ -1,23 +0,0 @@
import RuTorrent from "rutorrent-promise";
// TODO: Remove the 3rd party dependency once I figure out how to
// call this myself with fetch. Just need to destruct the package.
export default async function handler(req, res) {
const { url, username, password } = req.query;
const constructedUrl = new URL(url);
const rutorrent = new RuTorrent({
host: constructedUrl.hostname, // default: localhost
port: constructedUrl.port, // default: 80
path: constructedUrl.pathname, // default: /rutorrent
ssl: constructedUrl.protocol === "https:", // default: false
username: username, // default: none
password: password, // default: none
});
const data = await rutorrent.get(["d.get_down_rate", "d.get_up_rate", "d.get_state"]);
res.status(200).send(data);
}

View File

@@ -1,9 +1,28 @@
import cachedFetch from "utils/cached-fetch";
import { getSettings } from "utils/config";
export default async function handler(req, res) {
const { lat, lon, apiKey, duration } = req.query;
const { latitude, longitude, provider, cache } = req.query;
let { apiKey } = req.query;
const api_url = `http://api.weatherapi.com/v1/current.json?q=${lat},${lon}&key=${apiKey}`;
if (!apiKey && !provider) {
return res.status(400).json({ error: "Missing API key or provider" });
}
res.send(await cachedFetch(api_url, duration));
if (!apiKey && provider !== "weatherapi") {
return res.status(400).json({ error: "Invalid provider for endpoint" });
}
if (!apiKey && provider) {
const settings = await getSettings();
apiKey = settings?.providers?.weatherapi;
}
if (!apiKey) {
return res.status(400).json({ error: "Missing API key" });
}
const api_url = `http://api.weatherapi.com/v1/current.json?q=${latitude},${longitude}&key=${apiKey}`;
res.send(await cachedFetch(api_url, cache));
}

View File

@@ -0,0 +1,6 @@
# For configuration options and examples, please see:
# https://github.com/benphelps/homepage/wiki/Settings
providers:
openweathermap: openweathermapapikey
weatherapi: weatherapiapikey

35
src/utils/api-helpers.js Normal file
View File

@@ -0,0 +1,35 @@
const formats = {
emby: `{url}/emby/{endpoint}?api_key={key}`,
jellyfin: `{url}/emby/{endpoint}?api_key={key}`,
pihole: `{url}/admin/{endpoint}`,
radarr: `{url}/api/v3/{endpoint}?apikey={key}`,
sonarr: `{url}/api/v3/{endpoint}?apikey={key}`,
speedtest: `{url}/api/{endpoint}`,
tautulli: `{url}/api/v2?apikey={key}&cmd={endpoint}`,
traefik: `{url}/api/{endpoint}`,
portainer: `{url}/api/endpoints/{env}/{endpoint}`,
rutorrent: `{url}/plugins/httprpc/action.php`,
jellyseerr: `{url}/api/v1/{endpoint}`,
ombi: `{url}/api/v1/{endpoint}`,
npm: `{url}/api/{endpoint}`,
};
export function formatApiCall(api, args) {
const match = /\{.*?\}/g;
const replace = (match) => {
const key = match.replace(/\{|\}/g, "");
return args[key];
};
return formats[api].replace(match, replace);
}
export function formatApiUrl(widget, endpoint) {
const params = new URLSearchParams({
type: widget.type,
group: widget.service_group,
service: widget.service_name,
endpoint,
});
return `/api/services/proxy?${params.toString()}`;
}

View File

@@ -1,5 +1,6 @@
import { join } from "path";
import { existsSync, copyFile } from "fs";
import { existsSync, copyFile, promises as fs } from "fs";
import yaml from "js-yaml";
export default function checkAndCopyConfig(config) {
const configYaml = join(process.cwd(), "config", config);
@@ -14,3 +15,11 @@ export default function checkAndCopyConfig(config) {
});
}
}
export async function getSettings() {
checkAndCopyConfig("settings.yaml");
const settingsYaml = join(process.cwd(), "config", "settings.yaml");
const fileContents = await fs.readFile(settingsYaml, "utf8");
return yaml.load(fileContents);
}

View File

@@ -44,3 +44,20 @@ export function httpRequest(url, params) {
request.end();
});
}
export function httpProxy(url, params = {}) {
const constructedUrl = new URL(url);
if (constructedUrl.protocol === "https:") {
const httpsAgent = new https.Agent({
rejectUnauthorized: false,
});
return httpsRequest(constructedUrl, {
agent: httpsAgent,
...params,
});
} else {
return httpRequest(constructedUrl, params);
}
}

View File

@@ -0,0 +1,28 @@
import { getServiceWidget } from "utils/service-helpers";
import { formatApiCall } from "utils/api-helpers";
import { httpProxy } from "utils/http";
export default async function credentialedProxyHandler(req, res) {
const { group, service, endpoint } = req.query;
if (group && service) {
const widget = await getServiceWidget(group, service);
if (widget) {
const url = new URL(formatApiCall(widget.type, { endpoint, ...widget }));
const [status, contentType, data] = await httpProxy(url, {
withCredentials: true,
credentials: "include",
headers: {
"X-API-Key": `${widget.key}`,
"Content-Type": "application/json",
},
});
if (contentType) res.setHeader("Content-Type", contentType);
return res.status(status).send(data);
}
}
return res.status(400).json({ error: "Invalid proxy service type" });
}

View File

@@ -0,0 +1,21 @@
import { getServiceWidget } from "utils/service-helpers";
import { formatApiCall } from "utils/api-helpers";
import { httpProxy } from "utils/http";
export default async function genericProxyHandler(req, res) {
const { group, service, endpoint } = req.query;
if (group && service) {
const widget = await getServiceWidget(group, service);
if (widget) {
const url = new URL(formatApiCall(widget.type, { endpoint, ...widget }));
const [status, contentType, data] = await httpProxy(url);
if (contentType) res.setHeader("Content-Type", contentType);
return res.status(status).send(data);
}
}
return res.status(400).json({ error: "Invalid proxy service type" });
}

37
src/utils/proxies/npm.js Normal file
View File

@@ -0,0 +1,37 @@
import { getServiceWidget } from "utils/service-helpers";
import { formatApiCall } from "utils/api-helpers";
export default async function npmProxyHandler(req, res) {
const { group, service, endpoint } = req.query;
if (group && service) {
const widget = await getServiceWidget(group, service);
if (widget) {
const url = new URL(formatApiCall(widget.type, { endpoint, ...widget }));
const loginUrl = `${widget.url}/api/tokens`;
const body = { identity: widget.username, secret: widget.password };
const authResponse = await fetch(loginUrl, {
method: "POST",
body: JSON.stringify(body),
headers: {
"Content-Type": "application/json",
},
}).then((response) => response.json());
const apiResponse = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + authResponse.token,
},
}).then((response) => response.json());
return res.send(apiResponse);
}
}
return res.status(400).json({ error: "Invalid proxy service type" });
}

View File

@@ -0,0 +1,39 @@
import { JSONRPCClient } from "json-rpc-2.0";
import { getServiceWidget } from "utils/service-helpers";
export default async function nzbgetProxyHandler(req, res) {
const { group, service, endpoint } = req.query;
if (group && service) {
const widget = await getServiceWidget(group, service);
if (widget) {
const constructedUrl = new URL(widget.url);
constructedUrl.pathname = "jsonrpc";
const authorization = Buffer.from(`${widget.username}:${widget.password}`).toString("base64");
const client = new JSONRPCClient((jsonRPCRequest) =>
fetch(constructedUrl.toString(), {
method: "POST",
headers: {
"content-type": "application/json",
authorization: `Basic ${authorization}`,
},
body: JSON.stringify(jsonRPCRequest),
}).then(async (response) => {
if (response.status === 200) {
const jsonRPCResponse = await response.json();
return client.receive(jsonRPCResponse);
} else if (jsonRPCRequest.id !== undefined) {
return Promise.reject(new Error(response.statusText));
}
})
);
return res.send(await client.request(endpoint));
}
}
return res.status(400).json({ error: "Invalid proxy service type" });
}

View File

@@ -0,0 +1,30 @@
import RuTorrent from "rutorrent-promise";
import { getServiceWidget } from "utils/service-helpers";
export default async function rutorrentProxyHandler(req, res) {
const { group, service } = req.query;
if (group && service) {
const widget = await getServiceWidget(group, service);
if (widget) {
const constructedUrl = new URL(widget.url);
const rutorrent = new RuTorrent({
host: constructedUrl.hostname,
port: constructedUrl.port,
path: constructedUrl.pathname,
ssl: constructedUrl.protocol === "https:",
username: widget.username,
password: widget.password,
});
const data = await rutorrent.get(["d.get_down_rate", "d.get_up_rate", "d.get_state"]);
return res.status(200).send(data);
}
}
return res.status(400).json({ error: "Invalid proxy service type" });
}

View File

@@ -0,0 +1,33 @@
import { promises as fs } from "fs";
import path from "path";
import yaml from "js-yaml";
export async function getServiceWidget(group, service) {
const servicesYaml = path.join(process.cwd(), "config", "services.yaml");
const fileContents = await fs.readFile(servicesYaml, "utf8");
const services = yaml.load(fileContents);
// map easy to write YAML objects into easy to consume JS arrays
const servicesArray = services.map((group) => {
return {
name: Object.keys(group)[0],
services: group[Object.keys(group)[0]].map((entries) => {
return {
name: Object.keys(entries)[0],
...entries[Object.keys(entries)[0]],
};
}),
};
});
const serviceGroup = servicesArray.find((g) => g.name === group);
if (serviceGroup) {
const serviceEntry = serviceGroup.services.find((s) => s.name === service);
if (serviceEntry) {
const { widget } = serviceEntry;
return widget;
}
}
return false;
}