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

@@ -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(),
}),
);