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

@ -16,6 +16,7 @@ specifiers:
eslint-plugin-prettier: ^4.2.1
eslint-plugin-react: ^7.31.8
eslint-plugin-react-hooks: ^4.6.0
follow-redirects: ^1.15.2
i18next: ^21.9.1
i18next-browser-languagedetector: ^6.1.5
i18next-http-backend: ^1.4.1
@ -36,6 +37,7 @@ specifiers:
shvl: ^3.0.0
swr: ^1.3.0
tailwindcss: ^3.1.8
tough-cookie: ^4.1.2
typescript: ^4.8.3
dependencies:
@ -44,6 +46,7 @@ dependencies:
classnames: 2.3.1
currency-symbol-map: 5.1.0
dockerode: 3.3.4
follow-redirects: 1.15.2
i18next: 21.9.1
i18next-browser-languagedetector: 6.1.5
i18next-http-backend: 1.4.1
@ -61,6 +64,7 @@ dependencies:
rutorrent-promise: 2.0.0
shvl: 3.0.0
swr: 1.3.0_react@18.2.0
tough-cookie: 4.1.2
devDependencies:
autoprefixer: 10.4.9_postcss@8.4.16
@ -1358,6 +1362,16 @@ packages:
resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==}
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:
resolution: {integrity: sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==}
engines: {node: '>= 6'}
@ -2162,6 +2176,10 @@ packages:
react-is: 16.13.1
dev: true
/psl/1.9.0:
resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==}
dev: false
/pump/3.0.0:
resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==}
dependencies:
@ -2172,7 +2190,10 @@ packages:
/punycode/2.1.1:
resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==}
engines: {node: '>=6'}
dev: true
/querystringify/2.2.0:
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
dev: false
/queue-microtask/1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
@ -2277,6 +2298,10 @@ packages:
engines: {node: '>=8'}
dev: true
/requires-port/1.0.0:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
dev: false
/resolve-from/4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
@ -2563,6 +2588,16 @@ packages:
engines: {node: '>=0.6'}
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:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
dev: false
@ -2625,6 +2660,11 @@ packages:
which-boxed-primitive: 1.0.2
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:
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
engines: {node: '>= 0.8'}
@ -2647,6 +2687,13 @@ packages:
punycode: 2.1.1
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:
resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
peerDependencies:

@ -139,5 +139,9 @@
"numberOfQueries": "Queries",
"numberOfFailGrabs": "Fail Grabs",
"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 Gotify from "./widgets/service/gotify";
import Prowlarr from "./widgets/service/prowlarr";
import Jackett from "./widgets/service/jackett";
const widgetMappings = {
docker: Docker,
@ -45,7 +46,8 @@ const widgetMappings = {
npm: Npm,
tautulli: Tautulli,
gotify: Gotify,
prowlarr: Prowlarr
prowlarr: Prowlarr,
jackett: Jackett
};
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,
traefik: genericProxyHandler,
sabnzbd: genericProxyHandler,
jackett: genericProxyHandler,
// uses X-API-Key (or similar) header auth
gotify: credentialedProxyHandler,
portainer: credentialedProxyHandler,

@ -19,6 +19,7 @@ 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`
};
export function formatApiCall(api, args) {

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

Loading…
Cancel
Save