diff --git a/package.json b/package.json
index 6acc6a4ef..dd9f7f87a 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,7 @@
"@tailwindcss/forms": "^0.5.3",
"classnames": "^2.3.1",
"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",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 32becdd4c..193092180 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -15,6 +15,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
@@ -35,6 +36,7 @@ specifiers:
shvl: ^3.0.0
swr: ^1.3.0
tailwindcss: ^3.1.8
+ tough-cookie: ^4.1.2
typescript: ^4.8.3
dependencies:
@@ -42,6 +44,7 @@ dependencies:
'@tailwindcss/forms': 0.5.3_tailwindcss@3.1.8
classnames: 2.3.1
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
@@ -59,6 +62,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
@@ -1352,6 +1356,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'}
@@ -2156,6 +2170,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:
@@ -2166,7 +2184,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==}
@@ -2271,6 +2292,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'}
@@ -2557,6 +2582,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
@@ -2619,6 +2654,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'}
@@ -2641,6 +2681,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:
diff --git a/public/locales/en/common.json b/public/locales/en/common.json
index 60bfef6b7..b81009351 100644
--- a/public/locales/en/common.json
+++ b/public/locales/en/common.json
@@ -143,5 +143,9 @@
"numberOfQueries": "Queries",
"numberOfFailGrabs": "Fail Grabs",
"numberOfFailQueries": "Fail Queries"
+ },
+ "jackett": {
+ "configured": "Configured",
+ "errored": "Errored"
}
}
diff --git a/src/components/services/widget.jsx b/src/components/services/widget.jsx
index df1243240..0bb5c08d5 100644
--- a/src/components/services/widget.jsx
+++ b/src/components/services/widget.jsx
@@ -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,
@@ -46,6 +47,7 @@ const widgetMappings = {
tautulli: Tautulli,
gotify: Gotify,
prowlarr: Prowlarr,
+ jackett: Jackett,
};
export default function Widget({ service }) {
diff --git a/src/components/services/widgets/service/jackett.jsx b/src/components/services/widgets/service/jackett.jsx
new file mode 100644
index 000000000..c6583711d
--- /dev/null
+++ b/src/components/services/widgets/service/jackett.jsx
@@ -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 ;
+ }
+
+ if (!indexersData) {
+ return (
+
+
+
+
+ );
+ }
+
+ const errored = indexersData.filter((indexer) => indexer.last_error);
+
+ return (
+
+
+
+
+ );
+}
diff --git a/src/pages/api/services/proxy.js b/src/pages/api/services/proxy.js
index 8fe036f4f..9e3a57ff8 100644
--- a/src/pages/api/services/proxy.js
+++ b/src/pages/api/services/proxy.js
@@ -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,
diff --git a/src/utils/api-helpers.js b/src/utils/api-helpers.js
index 13d520bf3..2c1929df9 100644
--- a/src/utils/api-helpers.js
+++ b/src/utils/api-helpers.js
@@ -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) {
diff --git a/src/utils/http.js b/src/utils/http.js
index 76e882ed6..9f663cd43 100644
--- a/src/utils/http.js
+++ b/src/utils/http.js
@@ -1,9 +1,46 @@
/* 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 setCookieHeader(url, params) {
+ // add cookie header, if we have one in the jar
+ const existingCookie = cookieJar.getCookieStringSync(url.toString());
+ if (existingCookie) {
+ params.headers = params.headers ?? {};
+ params.headers.Cookie = existingCookie;
+ }
+}
+
+function addCookieHandler(url, params) {
+ 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);
+ }
+
+ setCookieHeader(options.href, options);
+ };
+}
export function httpsRequest(url, params) {
return new Promise((resolve, reject) => {
+ addCookieHandler(url, params);
const request = https.request(url, params, (response) => {
const data = [];
@@ -30,6 +67,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 = [];