feat: Prowlarr integration (#965)

Co-authored-by: Manuel <30572287+manuel-rw@users.noreply.github.com>
This commit is contained in:
Yossi Hillali
2024-09-01 16:40:22 +03:00
committed by GitHub
parent acb4cb9c82
commit 6ff36405ba
34 changed files with 346 additions and 212 deletions

View File

@@ -1,7 +1,9 @@
{
"name": "@homarr/integrations",
"private": true,
"version": "0.1.0",
"private": true,
"license": "MIT",
"type": "module",
"exports": {
".": "./index.ts",
"./client": "./src/client.ts",
@@ -14,21 +16,20 @@
]
}
},
"license": "MIT",
"type": "module",
"scripts": {
"clean": "rm -rf .turbo node_modules",
"lint": "eslint",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@jellyfin/sdk": "^0.10.0",
"@homarr/translation": "workspace:^0.1.0"
"@jellyfin/sdk": "^0.10.0"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
@@ -36,6 +37,5 @@
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.9.1",
"typescript": "^5.5.4"
},
"prettier": "@homarr/prettier-config"
}
}

View File

@@ -7,6 +7,7 @@ import { JellyseerrIntegration } from "../jellyseerr/jellyseerr-integration";
import { SonarrIntegration } from "../media-organizer/sonarr/sonarr-integration";
import { OverseerrIntegration } from "../overseerr/overseerr-integration";
import { PiHoleIntegration } from "../pi-hole/pi-hole-integration";
import { ProwlarrIntegration } from "../prowlarr/prowlarr-integration";
import type { Integration, IntegrationInput } from "./integration";
export const integrationCreatorByKind = <TKind extends keyof typeof integrationCreators>(
@@ -28,4 +29,5 @@ export const integrationCreators = {
sonarr: SonarrIntegration,
jellyseerr: JellyseerrIntegration,
overseerr: OverseerrIntegration,
prowlarr: ProwlarrIntegration,
} satisfies Partial<Record<IntegrationKind, new (integration: IntegrationInput) => Integration>>;

View File

@@ -0,0 +1,12 @@
export interface Indexer {
id: number;
name: string;
url: string;
/**
* Enabled: when the user enable / disable the indexer.
* Status: when there is an error with the indexer site.
* If one of the options are false the indexer is off.
*/
enabled: boolean;
status: boolean;
}

View File

@@ -0,0 +1,99 @@
import { Integration } from "../base/integration";
import { IntegrationTestConnectionError } from "../base/test-connection-error";
import type { Indexer } from "../interfaces/indexer-manager/indexer";
import { indexerResponseSchema, statusResponseSchema } from "./prowlarr-types";
export class ProwlarrIntegration extends Integration {
public async getIndexersAsync(): Promise<Indexer[]> {
const apiKey = super.getSecretValue("apiKey");
const indexerResponse = await fetch(`${this.integration.url}/api/v1/indexer`, {
headers: {
"X-Api-Key": apiKey,
},
});
if (!indexerResponse.ok) {
throw new Error(
`Failed to fetch indexers for ${this.integration.name} (${this.integration.id}): ${indexerResponse.statusText}`,
);
}
const statusResponse = await fetch(`${this.integration.url}/api/v1/indexerstatus`, {
headers: {
"X-Api-Key": apiKey,
},
});
if (!statusResponse.ok) {
throw new Error(
`Failed to fetch status for ${this.integration.name} (${this.integration.id}): ${statusResponse.statusText}`,
);
}
const indexersResult = indexerResponseSchema.array().safeParse(await indexerResponse.json());
const statusResult = statusResponseSchema.safeParse(await statusResponse.json());
const errorMessages: string[] = [];
if (!indexersResult.success) {
errorMessages.push(`Indexers parsing error: ${indexersResult.error.message}`);
}
if (!statusResult.success) {
errorMessages.push(`Status parsing error: ${statusResult.error.message}`);
}
if (!indexersResult.success || !statusResult.success) {
throw new Error(
`Failed to parse indexers for ${this.integration.name} (${this.integration.id}), most likely your api key is wrong:\n${errorMessages.join("\n")}`,
);
}
const inactiveIndexerIds = new Set(statusResult.data.map((status: { id: number }) => status.id));
const indexers: Indexer[] = indexersResult.data.map((indexer) => ({
id: indexer.id,
name: indexer.name,
url: indexer.indexerUrls[0] ?? "",
enabled: indexer.enable,
status: inactiveIndexerIds.has(indexer.id),
}));
return indexers;
}
public async testAllAsync(): Promise<void> {
const apiKey = super.getSecretValue("apiKey");
const response = await fetch(`${this.integration.url}/api/v1/indexer/testall`, {
headers: {
"X-Api-Key": apiKey,
},
});
if (!response.ok) {
throw new Error(
`Failed to test all indexers for ${this.integration.name} (${this.integration.id}): ${response.statusText}`,
);
}
}
public async testConnectionAsync(): Promise<void> {
const apiKey = super.getSecretValue("apiKey");
await super.handleTestConnectionResponseAsync({
queryFunctionAsync: async () => {
return await fetch(`${this.integration.url}/api`, {
headers: {
"X-Api-Key": apiKey,
},
});
},
handleResponseAsync: async (response) => {
try {
const result = (await response.json()) as unknown;
if (typeof result === "object" && result !== null) return;
} catch {
throw new IntegrationTestConnectionError("invalidJson");
}
throw new IntegrationTestConnectionError("invalidCredentials");
},
});
}
}

View File

@@ -0,0 +1,14 @@
import { z } from "@homarr/validation";
export const indexerResponseSchema = z.object({
id: z.number(),
indexerUrls: z.array(z.string()),
name: z.string(),
enable: z.boolean(),
});
export const statusResponseSchema = z.array(
z.object({
id: z.number(),
}),
);