Add the Jackett widget

- add the follow-redirect package
- add the tough-cookie package

Jackett API uses a redirect mechanism to set a CSRF token.
This CSRF token is stored in a cookie that is required to
be present or the API won't work.
pull/156/head
Jason Fischer 2 years ago
parent 945ed854a4
commit f750876425

@ -14,6 +14,7 @@
"classnames": "^2.3.1", "classnames": "^2.3.1",
"currency-symbol-map": "^5.1.0", "currency-symbol-map": "^5.1.0",
"dockerode": "^3.3.4", "dockerode": "^3.3.4",
"follow-redirects": "^1.15.2",
"i18next": "^21.9.1", "i18next": "^21.9.1",
"i18next-browser-languagedetector": "^6.1.5", "i18next-browser-languagedetector": "^6.1.5",
"i18next-http-backend": "^1.4.1", "i18next-http-backend": "^1.4.1",
@ -30,7 +31,8 @@
"react-icons": "^4.4.0", "react-icons": "^4.4.0",
"rutorrent-promise": "^2.0.0", "rutorrent-promise": "^2.0.0",
"shvl": "^3.0.0", "shvl": "^3.0.0",
"swr": "^1.3.0" "swr": "^1.3.0",
"tough-cookie": "^4.1.2"
}, },
"devDependencies": { "devDependencies": {
"autoprefixer": "^10.4.9", "autoprefixer": "^10.4.9",

@ -16,6 +16,7 @@ specifiers:
eslint-plugin-prettier: ^4.2.1 eslint-plugin-prettier: ^4.2.1
eslint-plugin-react: ^7.31.8 eslint-plugin-react: ^7.31.8
eslint-plugin-react-hooks: ^4.6.0 eslint-plugin-react-hooks: ^4.6.0
follow-redirects: ^1.15.2
i18next: ^21.9.1 i18next: ^21.9.1
i18next-browser-languagedetector: ^6.1.5 i18next-browser-languagedetector: ^6.1.5
i18next-http-backend: ^1.4.1 i18next-http-backend: ^1.4.1
@ -36,6 +37,7 @@ specifiers:
shvl: ^3.0.0 shvl: ^3.0.0
swr: ^1.3.0 swr: ^1.3.0
tailwindcss: ^3.1.8 tailwindcss: ^3.1.8
tough-cookie: ^4.1.2
typescript: ^4.8.3 typescript: ^4.8.3
dependencies: dependencies:
@ -44,6 +46,7 @@ dependencies:
classnames: 2.3.1 classnames: 2.3.1
currency-symbol-map: 5.1.0 currency-symbol-map: 5.1.0
dockerode: 3.3.4 dockerode: 3.3.4
follow-redirects: 1.15.2
i18next: 21.9.1 i18next: 21.9.1
i18next-browser-languagedetector: 6.1.5 i18next-browser-languagedetector: 6.1.5
i18next-http-backend: 1.4.1 i18next-http-backend: 1.4.1
@ -61,6 +64,7 @@ dependencies:
rutorrent-promise: 2.0.0 rutorrent-promise: 2.0.0
shvl: 3.0.0 shvl: 3.0.0
swr: 1.3.0_react@18.2.0 swr: 1.3.0_react@18.2.0
tough-cookie: 4.1.2
devDependencies: devDependencies:
autoprefixer: 10.4.9_postcss@8.4.16 autoprefixer: 10.4.9_postcss@8.4.16
@ -1358,6 +1362,16 @@ packages:
resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==}
dev: true dev: true
/follow-redirects/1.15.2:
resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
dev: false
/form-data/3.0.1: /form-data/3.0.1:
resolution: {integrity: sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==} resolution: {integrity: sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
@ -2162,6 +2176,10 @@ packages:
react-is: 16.13.1 react-is: 16.13.1
dev: true dev: true
/psl/1.9.0:
resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==}
dev: false
/pump/3.0.0: /pump/3.0.0:
resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==}
dependencies: dependencies:
@ -2172,7 +2190,10 @@ packages:
/punycode/2.1.1: /punycode/2.1.1:
resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==} resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==}
engines: {node: '>=6'} engines: {node: '>=6'}
dev: true
/querystringify/2.2.0:
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
dev: false
/queue-microtask/1.2.3: /queue-microtask/1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
@ -2277,6 +2298,10 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
dev: true dev: true
/requires-port/1.0.0:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
dev: false
/resolve-from/4.0.0: /resolve-from/4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'} engines: {node: '>=4'}
@ -2563,6 +2588,16 @@ packages:
engines: {node: '>=0.6'} engines: {node: '>=0.6'}
dev: false dev: false
/tough-cookie/4.1.2:
resolution: {integrity: sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==}
engines: {node: '>=6'}
dependencies:
psl: 1.9.0
punycode: 2.1.1
universalify: 0.2.0
url-parse: 1.5.10
dev: false
/tr46/0.0.3: /tr46/0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
dev: false dev: false
@ -2625,6 +2660,11 @@ packages:
which-boxed-primitive: 1.0.2 which-boxed-primitive: 1.0.2
dev: true dev: true
/universalify/0.2.0:
resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
engines: {node: '>= 4.0.0'}
dev: false
/unpipe/1.0.0: /unpipe/1.0.0:
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@ -2647,6 +2687,13 @@ packages:
punycode: 2.1.1 punycode: 2.1.1
dev: true dev: true
/url-parse/1.5.10:
resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
dependencies:
querystringify: 2.2.0
requires-port: 1.0.0
dev: false
/use-sync-external-store/1.2.0_react@18.2.0: /use-sync-external-store/1.2.0_react@18.2.0:
resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
peerDependencies: peerDependencies:

@ -139,5 +139,9 @@
"numberOfQueries": "Queries", "numberOfQueries": "Queries",
"numberOfFailGrabs": "Fail Grabs", "numberOfFailGrabs": "Fail Grabs",
"numberOfFailQueries": "Fail Queries" "numberOfFailQueries": "Fail Queries"
},
"jackett": {
"configured": "Configured",
"errored": "Errored"
} }
} }

@ -22,6 +22,7 @@ import Tautulli from "./widgets/service/tautulli";
import CoinMarketCap from "./widgets/service/coinmarketcap"; import CoinMarketCap from "./widgets/service/coinmarketcap";
import Gotify from "./widgets/service/gotify"; import Gotify from "./widgets/service/gotify";
import Prowlarr from "./widgets/service/prowlarr"; import Prowlarr from "./widgets/service/prowlarr";
import Jackett from "./widgets/service/jackett";
const widgetMappings = { const widgetMappings = {
docker: Docker, docker: Docker,
@ -45,7 +46,8 @@ const widgetMappings = {
npm: Npm, npm: Npm,
tautulli: Tautulli, tautulli: Tautulli,
gotify: Gotify, gotify: Gotify,
prowlarr: Prowlarr prowlarr: Prowlarr,
jackett: Jackett
}; };
export default function Widget({ service }) { export default function Widget({ service }) {

@ -0,0 +1,37 @@
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 Jackett({ service }) {
const { t } = useTranslation();
const config = service.widget;
const { data: indexersData, error: indexersError } = useSWR(formatApiUrl(config, "indexers"));
if (indexersError) {
return <Widget error={t("widget.api_error")} />;
}
if (!indexersData) {
return (
<Widget>
<Block label={t("jackett.configured")} />
<Block label={t("jackett.errored")} />
</Widget>
);
}
const errored = indexersData.filter((indexer) => indexer.last_error);
return (
<Widget>
<Block label={t("jackett.configured")} value={indexersData.length} />
<Block label={t("jackett.errored")} value={errored.length} />
</Widget>
);
}

@ -17,6 +17,7 @@ const serviceProxyHandlers = {
tautulli: genericProxyHandler, tautulli: genericProxyHandler,
traefik: genericProxyHandler, traefik: genericProxyHandler,
sabnzbd: genericProxyHandler, sabnzbd: genericProxyHandler,
jackett: genericProxyHandler,
// uses X-API-Key (or similar) header auth // uses X-API-Key (or similar) header auth
gotify: credentialedProxyHandler, gotify: credentialedProxyHandler,
portainer: credentialedProxyHandler, portainer: credentialedProxyHandler,

@ -19,6 +19,7 @@ const formats = {
coinmarketcap: `https://pro-api.coinmarketcap.com/{endpoint}`, coinmarketcap: `https://pro-api.coinmarketcap.com/{endpoint}`,
gotify: `{url}/{endpoint}`, gotify: `{url}/{endpoint}`,
prowlarr: `{url}/api/v1/{endpoint}`, prowlarr: `{url}/api/v1/{endpoint}`,
jackett: `{url}/api/v2.0/{endpoint}?apikey={key}&configured=true`
}; };
export function formatApiCall(api, args) { export function formatApiCall(api, args) {

@ -1,9 +1,44 @@
/* eslint-disable prefer-promise-reject-errors */ /* eslint-disable prefer-promise-reject-errors */
import https from "https"; /* eslint-disable no-param-reassign */
import http from "http"; import { http, https } from "follow-redirects";
import { Cookie, CookieJar } from 'tough-cookie';
const cookieJar = new CookieJar();
function addCookieHandler(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;
}
// 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);
}
const cookie = cookieJar.getCookieStringSync(options.href);
options.headers = options.headers ?? {};
options.headers.Cookie = cookie;
};
}
export function httpsRequest(url, params) { export function httpsRequest(url, params) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
addCookieHandler(url, params);
const request = https.request(url, params, (response) => { const request = https.request(url, params, (response) => {
const data = []; const data = [];
@ -30,6 +65,7 @@ export function httpsRequest(url, params) {
export function httpRequest(url, params) { export function httpRequest(url, params) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
addCookieHandler(url, params);
const request = http.request(url, params, (response) => { const request = http.request(url, params, (response) => {
const data = []; const data = [];

Loading…
Cancel
Save