feat: Prowlarr integration (#965)
Co-authored-by: Manuel <30572287+manuel-rw@users.noreply.github.com>
This commit is contained in:
@@ -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>>;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
99
packages/integrations/src/prowlarr/prowlarr-integration.ts
Normal file
99
packages/integrations/src/prowlarr/prowlarr-integration.ts
Normal 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");
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
14
packages/integrations/src/prowlarr/prowlarr-types.ts
Normal file
14
packages/integrations/src/prowlarr/prowlarr-types.ts
Normal 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(),
|
||||
}),
|
||||
);
|
||||
Reference in New Issue
Block a user