fix: trailing slash integration url issues (#1571)
This commit is contained in:
@@ -1,12 +1,6 @@
|
|||||||
import type { ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers";
|
import type { ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers";
|
||||||
|
|
||||||
export const appendPath = (url: URL | string, path: string) => {
|
export const removeTrailingSlash = (path: string) => {
|
||||||
const newUrl = new URL(url);
|
|
||||||
newUrl.pathname = removeTrailingSlash(newUrl.pathname) + path;
|
|
||||||
return newUrl;
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeTrailingSlash = (path: string) => {
|
|
||||||
return path.at(-1) === "/" ? path.substring(0, path.length - 1) : path;
|
return path.at(-1) === "/" ? path.substring(0, path.length - 1) : path;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { filteringStatusSchema, statsResponseSchema, statusResponseSchema } from
|
|||||||
|
|
||||||
export class AdGuardHomeIntegration extends Integration implements DnsHoleSummaryIntegration {
|
export class AdGuardHomeIntegration extends Integration implements DnsHoleSummaryIntegration {
|
||||||
public async getSummaryAsync(): Promise<DnsHoleSummary> {
|
public async getSummaryAsync(): Promise<DnsHoleSummary> {
|
||||||
const statsResponse = await fetch(`${this.integration.url}/control/stats`, {
|
const statsResponse = await fetch(this.url("/control/stats"), {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Basic ${this.getAuthorizationHeaderValue()}`,
|
Authorization: `Basic ${this.getAuthorizationHeaderValue()}`,
|
||||||
},
|
},
|
||||||
@@ -18,7 +18,7 @@ export class AdGuardHomeIntegration extends Integration implements DnsHoleSummar
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusResponse = await fetch(`${this.integration.url}/control/status`, {
|
const statusResponse = await fetch(this.url("/control/status"), {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Basic ${this.getAuthorizationHeaderValue()}`,
|
Authorization: `Basic ${this.getAuthorizationHeaderValue()}`,
|
||||||
},
|
},
|
||||||
@@ -30,7 +30,7 @@ export class AdGuardHomeIntegration extends Integration implements DnsHoleSummar
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteringStatusResponse = await fetch(`${this.integration.url}/control/filtering/status`, {
|
const filteringStatusResponse = await fetch(this.url("/control/filtering/status"), {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Basic ${this.getAuthorizationHeaderValue()}`,
|
Authorization: `Basic ${this.getAuthorizationHeaderValue()}`,
|
||||||
},
|
},
|
||||||
@@ -86,7 +86,7 @@ export class AdGuardHomeIntegration extends Integration implements DnsHoleSummar
|
|||||||
public async testConnectionAsync(): Promise<void> {
|
public async testConnectionAsync(): Promise<void> {
|
||||||
await super.handleTestConnectionResponseAsync({
|
await super.handleTestConnectionResponseAsync({
|
||||||
queryFunctionAsync: async () => {
|
queryFunctionAsync: async () => {
|
||||||
return await fetch(`${this.integration.url}/control/status`, {
|
return await fetch(this.url("/control/status"), {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Basic ${this.getAuthorizationHeaderValue()}`,
|
Authorization: `Basic ${this.getAuthorizationHeaderValue()}`,
|
||||||
},
|
},
|
||||||
@@ -106,7 +106,7 @@ export class AdGuardHomeIntegration extends Integration implements DnsHoleSummar
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async enableAsync(): Promise<void> {
|
public async enableAsync(): Promise<void> {
|
||||||
const response = await fetch(`${this.integration.url}/control/protection`, {
|
const response = await fetch(this.url("/control/protection"), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -124,7 +124,7 @@ export class AdGuardHomeIntegration extends Integration implements DnsHoleSummar
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async disableAsync(duration = 0): Promise<void> {
|
public async disableAsync(duration = 0): Promise<void> {
|
||||||
const response = await fetch(`${this.integration.url}/control/protection`, {
|
const response = await fetch(this.url("/control/protection"), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { extractErrorMessage } from "@homarr/common";
|
import { extractErrorMessage, removeTrailingSlash } from "@homarr/common";
|
||||||
import type { IntegrationSecretKind } from "@homarr/definitions";
|
import type { IntegrationSecretKind } from "@homarr/definitions";
|
||||||
import { logger } from "@homarr/log";
|
import { logger } from "@homarr/log";
|
||||||
import type { TranslationObject } from "@homarr/translation";
|
import type { TranslationObject } from "@homarr/translation";
|
||||||
@@ -29,6 +29,19 @@ export abstract class Integration {
|
|||||||
return secret.value;
|
return secret.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected url(path: `/${string}`, queryParams?: Record<string, string | Date | number | boolean>) {
|
||||||
|
const baseUrl = removeTrailingSlash(this.integration.url);
|
||||||
|
const url = new URL(`${baseUrl}${path}`);
|
||||||
|
|
||||||
|
if (queryParams) {
|
||||||
|
for (const [key, value] of Object.entries(queryParams)) {
|
||||||
|
url.searchParams.set(key, value instanceof Date ? value.toISOString() : value.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test the connection to the integration
|
* Test the connection to the integration
|
||||||
* @throws {IntegrationTestConnectionError} if the connection fails
|
* @throws {IntegrationTestConnectionError} if the connection fails
|
||||||
|
|||||||
@@ -89,9 +89,8 @@ export class DelugeIntegration extends DownloadClientIntegration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getClient() {
|
private getClient() {
|
||||||
const baseUrl = new URL(this.integration.url).href;
|
|
||||||
return new Deluge({
|
return new Deluge({
|
||||||
baseUrl,
|
baseUrl: this.url("/").toString(),
|
||||||
password: this.getSecretValue("password"),
|
password: this.getSecretValue("password"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,9 +92,9 @@ export class NzbGetIntegration extends DownloadClientIntegration {
|
|||||||
method: CallType,
|
method: CallType,
|
||||||
...params: Parameters<NzbGetClient[CallType]>
|
...params: Parameters<NzbGetClient[CallType]>
|
||||||
): Promise<ReturnType<NzbGetClient[CallType]>> {
|
): Promise<ReturnType<NzbGetClient[CallType]>> {
|
||||||
const url = new URL(this.integration.url);
|
const username = this.getSecretValue("username");
|
||||||
url.pathname += `${this.getSecretValue("username")}:${this.getSecretValue("password")}`;
|
const password = this.getSecretValue("password");
|
||||||
url.pathname += url.pathname.endsWith("/") ? "jsonrpc" : "/jsonrpc";
|
const url = this.url(`/${username}:${password}/jsonrpc`);
|
||||||
const body = JSON.stringify({ method, params });
|
const body = JSON.stringify({ method, params });
|
||||||
return await fetch(url, { method: "POST", body })
|
return await fetch(url, { method: "POST", body })
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
|
|||||||
@@ -70,9 +70,8 @@ export class QBitTorrentIntegration extends DownloadClientIntegration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getClient() {
|
private getClient() {
|
||||||
const baseUrl = new URL(this.integration.url).href;
|
|
||||||
return new QBittorrent({
|
return new QBittorrent({
|
||||||
baseUrl,
|
baseUrl: this.url("/").toString(),
|
||||||
username: this.getSecretValue("username"),
|
username: this.getSecretValue("username"),
|
||||||
password: this.getSecretValue("password"),
|
password: this.getSecretValue("password"),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ dayjs.extend(duration);
|
|||||||
export class SabnzbdIntegration extends DownloadClientIntegration {
|
export class SabnzbdIntegration extends DownloadClientIntegration {
|
||||||
public async testConnectionAsync(): Promise<void> {
|
public async testConnectionAsync(): Promise<void> {
|
||||||
//This is the one call that uses the least amount of data while requiring the api key
|
//This is the one call that uses the least amount of data while requiring the api key
|
||||||
await this.sabNzbApiCallAsync("translate", new URLSearchParams({ value: "ping" }));
|
await this.sabNzbApiCallAsync("translate", { value: "ping" });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus> {
|
public async getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus> {
|
||||||
@@ -75,7 +75,7 @@ export class SabnzbdIntegration extends DownloadClientIntegration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async pauseItemAsync({ id }: DownloadClientItem) {
|
public async pauseItemAsync({ id }: DownloadClientItem) {
|
||||||
await this.sabNzbApiCallAsync("queue", new URLSearchParams({ name: "pause", value: id }));
|
await this.sabNzbApiCallAsync("queue", { name: "pause", value: id });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async resumeQueueAsync() {
|
public async resumeQueueAsync() {
|
||||||
@@ -83,32 +83,29 @@ export class SabnzbdIntegration extends DownloadClientIntegration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async resumeItemAsync({ id }: DownloadClientItem): Promise<void> {
|
public async resumeItemAsync({ id }: DownloadClientItem): Promise<void> {
|
||||||
await this.sabNzbApiCallAsync("queue", new URLSearchParams({ name: "resume", value: id }));
|
await this.sabNzbApiCallAsync("queue", { name: "resume", value: id });
|
||||||
}
|
}
|
||||||
|
|
||||||
//Delete files prevented on completed files. https://github.com/sabnzbd/sabnzbd/issues/2754
|
//Delete files prevented on completed files. https://github.com/sabnzbd/sabnzbd/issues/2754
|
||||||
//Works on all other in downloading and post-processing.
|
//Works on all other in downloading and post-processing.
|
||||||
//Will stop working as soon as the finished files is moved to completed folder.
|
//Will stop working as soon as the finished files is moved to completed folder.
|
||||||
public async deleteItemAsync({ id, progress }: DownloadClientItem, fromDisk: boolean): Promise<void> {
|
public async deleteItemAsync({ id, progress }: DownloadClientItem, fromDisk: boolean): Promise<void> {
|
||||||
await this.sabNzbApiCallAsync(
|
await this.sabNzbApiCallAsync(progress !== 1 ? "queue" : "history", {
|
||||||
progress !== 1 ? "queue" : "history",
|
name: "delete",
|
||||||
new URLSearchParams({
|
archive: fromDisk ? "0" : "1",
|
||||||
name: "delete",
|
value: id,
|
||||||
archive: fromDisk ? "0" : "1",
|
del_files: fromDisk ? "1" : "0",
|
||||||
value: id,
|
});
|
||||||
del_files: fromDisk ? "1" : "0",
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async sabNzbApiCallAsync(mode: string, searchParams?: URLSearchParams): Promise<unknown> {
|
private async sabNzbApiCallAsync(mode: string, searchParams?: Record<string, string>): Promise<unknown> {
|
||||||
const url = new URL("api", this.integration.url);
|
const url = this.url("/api", {
|
||||||
url.searchParams.append("output", "json");
|
...searchParams,
|
||||||
url.searchParams.append("mode", mode);
|
output: "json",
|
||||||
searchParams?.forEach((value, key) => {
|
mode,
|
||||||
url.searchParams.append(key, value);
|
apikey: this.getSecretValue("apiKey"),
|
||||||
});
|
});
|
||||||
url.searchParams.append("apikey", this.getSecretValue("apiKey"));
|
|
||||||
return await fetch(url)
|
return await fetch(url)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|||||||
@@ -71,9 +71,8 @@ export class TransmissionIntegration extends DownloadClientIntegration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getClient() {
|
private getClient() {
|
||||||
const baseUrl = new URL(this.integration.url).href;
|
|
||||||
return new Transmission({
|
return new Transmission({
|
||||||
baseUrl,
|
baseUrl: this.url("/").toString(),
|
||||||
username: this.getSecretValue("username"),
|
username: this.getSecretValue("username"),
|
||||||
password: this.getSecretValue("password"),
|
password: this.getSecretValue("password"),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { appendPath } from "@homarr/common";
|
|
||||||
import { logger } from "@homarr/log";
|
import { logger } from "@homarr/log";
|
||||||
|
|
||||||
import { Integration } from "../base/integration";
|
import { Integration } from "../base/integration";
|
||||||
@@ -17,7 +16,7 @@ export class HomeAssistantIntegration extends Integration {
|
|||||||
}
|
}
|
||||||
return entityStateSchema.safeParseAsync(body);
|
return entityStateSchema.safeParseAsync(body);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`Failed to fetch from ${this.integration.url}: ${err as string}`);
|
logger.error(`Failed to fetch from ${this.url("/")}: ${err as string}`);
|
||||||
return {
|
return {
|
||||||
success: false as const,
|
success: false as const,
|
||||||
error: err,
|
error: err,
|
||||||
@@ -33,7 +32,7 @@ export class HomeAssistantIntegration extends Integration {
|
|||||||
|
|
||||||
return response.ok;
|
return response.ok;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`Failed to fetch from '${this.integration.url}': ${err as string}`);
|
logger.error(`Failed to fetch from '${this.url("/")}': ${err as string}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,7 +51,7 @@ export class HomeAssistantIntegration extends Integration {
|
|||||||
|
|
||||||
return response.ok;
|
return response.ok;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`Failed to fetch from '${this.integration.url}': ${err as string}`);
|
logger.error(`Failed to fetch from '${this.url("/")}': ${err as string}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,7 +71,7 @@ export class HomeAssistantIntegration extends Integration {
|
|||||||
* @returns the response from the API
|
* @returns the response from the API
|
||||||
*/
|
*/
|
||||||
private async getAsync(path: `/api/${string}`) {
|
private async getAsync(path: `/api/${string}`) {
|
||||||
return await fetch(appendPath(this.integration.url, path), {
|
return await fetch(this.url(path), {
|
||||||
headers: this.getAuthHeaders(),
|
headers: this.getAuthHeaders(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -85,7 +84,7 @@ export class HomeAssistantIntegration extends Integration {
|
|||||||
* @returns the response from the API
|
* @returns the response from the API
|
||||||
*/
|
*/
|
||||||
private async postAsync(path: `/api/${string}`, body: Record<string, string>) {
|
private async postAsync(path: `/api/${string}`, body: Record<string, string>) {
|
||||||
return await fetch(appendPath(this.integration.url, path), {
|
return await fetch(this.url(path), {
|
||||||
headers: this.getAuthHeaders(),
|
headers: this.getAuthHeaders(),
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
@@ -29,9 +29,7 @@ export class JellyfinIntegration extends Integration {
|
|||||||
const sessions = await sessionApi.getSessions();
|
const sessions = await sessionApi.getSessions();
|
||||||
|
|
||||||
if (sessions.status !== 200) {
|
if (sessions.status !== 200) {
|
||||||
throw new Error(
|
throw new Error(`Jellyfin server ${this.url("/")} returned a non successful status code: ${sessions.status}`);
|
||||||
`Jellyfin server ${this.integration.url} returned a non successful status code: ${sessions.status}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return sessions.data.map((sessionInfo): StreamSession => {
|
return sessions.data.map((sessionInfo): StreamSession => {
|
||||||
@@ -52,7 +50,7 @@ export class JellyfinIntegration extends Integration {
|
|||||||
sessionId: `${sessionInfo.Id}`,
|
sessionId: `${sessionInfo.Id}`,
|
||||||
sessionName: `${sessionInfo.Client} (${sessionInfo.DeviceName})`,
|
sessionName: `${sessionInfo.Client} (${sessionInfo.DeviceName})`,
|
||||||
user: {
|
user: {
|
||||||
profilePictureUrl: `${this.integration.url}/Users/${sessionInfo.UserId}/Images/Primary`,
|
profilePictureUrl: this.url(`/Users/${sessionInfo.UserId}/Images/Primary`).toString(),
|
||||||
userId: sessionInfo.UserId ?? "",
|
userId: sessionInfo.UserId ?? "",
|
||||||
username: sessionInfo.UserName ?? "",
|
username: sessionInfo.UserName ?? "",
|
||||||
},
|
},
|
||||||
@@ -63,6 +61,6 @@ export class JellyfinIntegration extends Integration {
|
|||||||
|
|
||||||
private getApi() {
|
private getApi() {
|
||||||
const apiKey = this.getSecretValue("apiKey");
|
const apiKey = this.getSecretValue("apiKey");
|
||||||
return this.jellyfin.createApi(this.integration.url, apiKey);
|
return this.jellyfin.createApi(this.url("/").toString(), apiKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export class LidarrIntegration extends MediaOrganizerIntegration {
|
|||||||
public async testConnectionAsync(): Promise<void> {
|
public async testConnectionAsync(): Promise<void> {
|
||||||
await super.handleTestConnectionResponseAsync({
|
await super.handleTestConnectionResponseAsync({
|
||||||
queryFunctionAsync: async () => {
|
queryFunctionAsync: async () => {
|
||||||
return await fetch(`${this.integration.url}/api`, {
|
return await fetch(this.url("/api"), {
|
||||||
headers: { "X-Api-Key": super.getSecretValue("apiKey") },
|
headers: { "X-Api-Key": super.getSecretValue("apiKey") },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -22,11 +22,12 @@ export class LidarrIntegration extends MediaOrganizerIntegration {
|
|||||||
* @param includeUnmonitored When true results will include unmonitored items of the Tadarr library.
|
* @param includeUnmonitored When true results will include unmonitored items of the Tadarr library.
|
||||||
*/
|
*/
|
||||||
async getCalendarEventsAsync(start: Date, end: Date, includeUnmonitored = true): Promise<CalendarEvent[]> {
|
async getCalendarEventsAsync(start: Date, end: Date, includeUnmonitored = true): Promise<CalendarEvent[]> {
|
||||||
const url = new URL(this.integration.url);
|
const url = this.url("/api/v1/calendar", {
|
||||||
url.pathname = "/api/v1/calendar";
|
start,
|
||||||
url.searchParams.append("start", start.toISOString());
|
end,
|
||||||
url.searchParams.append("end", end.toISOString());
|
unmonitored: includeUnmonitored,
|
||||||
url.searchParams.append("unmonitored", includeUnmonitored ? "true" : "false");
|
});
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Api-Key": super.getSecretValue("apiKey"),
|
"X-Api-Key": super.getSecretValue("apiKey"),
|
||||||
|
|||||||
@@ -14,11 +14,12 @@ export class RadarrIntegration extends MediaOrganizerIntegration {
|
|||||||
* @param includeUnmonitored When true results will include unmonitored items of the Tadarr library.
|
* @param includeUnmonitored When true results will include unmonitored items of the Tadarr library.
|
||||||
*/
|
*/
|
||||||
async getCalendarEventsAsync(start: Date, end: Date, includeUnmonitored = true): Promise<CalendarEvent[]> {
|
async getCalendarEventsAsync(start: Date, end: Date, includeUnmonitored = true): Promise<CalendarEvent[]> {
|
||||||
const url = new URL(this.integration.url);
|
const url = this.url("/api/v3/calendar", {
|
||||||
url.pathname = "/api/v3/calendar";
|
start,
|
||||||
url.searchParams.append("start", start.toISOString());
|
end,
|
||||||
url.searchParams.append("end", end.toISOString());
|
unmonitored: includeUnmonitored,
|
||||||
url.searchParams.append("unmonitored", includeUnmonitored ? "true" : "false");
|
});
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Api-Key": super.getSecretValue("apiKey"),
|
"X-Api-Key": super.getSecretValue("apiKey"),
|
||||||
@@ -48,7 +49,7 @@ export class RadarrIntegration extends MediaOrganizerIntegration {
|
|||||||
private getLinksForRadarrCalendarEvent = (event: z.infer<typeof radarrCalendarEventSchema>) => {
|
private getLinksForRadarrCalendarEvent = (event: z.infer<typeof radarrCalendarEventSchema>) => {
|
||||||
const links: CalendarEvent["links"] = [
|
const links: CalendarEvent["links"] = [
|
||||||
{
|
{
|
||||||
href: `${this.integration.url}/movie/${event.titleSlug}`,
|
href: this.url(`/movie/${event.titleSlug}`).toString(),
|
||||||
name: "Radarr",
|
name: "Radarr",
|
||||||
logo: "/images/apps/radarr.svg",
|
logo: "/images/apps/radarr.svg",
|
||||||
color: undefined,
|
color: undefined,
|
||||||
@@ -93,7 +94,7 @@ export class RadarrIntegration extends MediaOrganizerIntegration {
|
|||||||
public async testConnectionAsync(): Promise<void> {
|
public async testConnectionAsync(): Promise<void> {
|
||||||
await super.handleTestConnectionResponseAsync({
|
await super.handleTestConnectionResponseAsync({
|
||||||
queryFunctionAsync: async () => {
|
queryFunctionAsync: async () => {
|
||||||
return await fetch(`${this.integration.url}/api`, {
|
return await fetch(this.url("/api"), {
|
||||||
headers: { "X-Api-Key": super.getSecretValue("apiKey") },
|
headers: { "X-Api-Key": super.getSecretValue("apiKey") },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export class ReadarrIntegration extends MediaOrganizerIntegration {
|
|||||||
public async testConnectionAsync(): Promise<void> {
|
public async testConnectionAsync(): Promise<void> {
|
||||||
await super.handleTestConnectionResponseAsync({
|
await super.handleTestConnectionResponseAsync({
|
||||||
queryFunctionAsync: async () => {
|
queryFunctionAsync: async () => {
|
||||||
return await fetch(`${this.integration.url}/api`, {
|
return await fetch(this.url("/api"), {
|
||||||
headers: { "X-Api-Key": super.getSecretValue("apiKey") },
|
headers: { "X-Api-Key": super.getSecretValue("apiKey") },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -27,12 +27,13 @@ export class ReadarrIntegration extends MediaOrganizerIntegration {
|
|||||||
includeUnmonitored = true,
|
includeUnmonitored = true,
|
||||||
includeAuthor = true,
|
includeAuthor = true,
|
||||||
): Promise<CalendarEvent[]> {
|
): Promise<CalendarEvent[]> {
|
||||||
const url = new URL(this.integration.url);
|
const url = this.url("/api/v1/calendar", {
|
||||||
url.pathname = "/api/v1/calendar";
|
start,
|
||||||
url.searchParams.append("start", start.toISOString());
|
end,
|
||||||
url.searchParams.append("end", end.toISOString());
|
unmonitored: includeUnmonitored,
|
||||||
url.searchParams.append("unmonitored", includeUnmonitored.toString());
|
includeAuthor,
|
||||||
url.searchParams.append("includeAuthor", includeAuthor.toString());
|
});
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Api-Key": super.getSecretValue("apiKey"),
|
"X-Api-Key": super.getSecretValue("apiKey"),
|
||||||
@@ -58,7 +59,7 @@ export class ReadarrIntegration extends MediaOrganizerIntegration {
|
|||||||
private getLinksForReadarrCalendarEvent = (event: z.infer<typeof readarrCalendarEventSchema>) => {
|
private getLinksForReadarrCalendarEvent = (event: z.infer<typeof readarrCalendarEventSchema>) => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
href: `${this.integration.url}/author/${event.author.foreignAuthorId}`,
|
href: this.url(`/author/${event.author.foreignAuthorId}`).toString(),
|
||||||
color: "#f5c518",
|
color: "#f5c518",
|
||||||
isDark: false,
|
isDark: false,
|
||||||
logo: "/images/apps/readarr.svg",
|
logo: "/images/apps/readarr.svg",
|
||||||
@@ -85,7 +86,7 @@ export class ReadarrIntegration extends MediaOrganizerIntegration {
|
|||||||
if (!bestImage) {
|
if (!bestImage) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return `${this.integration.url}${bestImage.url}`;
|
return this.url(bestImage.url as `/${string}`).toString();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,14 +12,15 @@ export class SonarrIntegration extends MediaOrganizerIntegration {
|
|||||||
* @param includeUnmonitored When true results will include unmonitored items of the Sonarr library.
|
* @param includeUnmonitored When true results will include unmonitored items of the Sonarr library.
|
||||||
*/
|
*/
|
||||||
async getCalendarEventsAsync(start: Date, end: Date, includeUnmonitored = true): Promise<CalendarEvent[]> {
|
async getCalendarEventsAsync(start: Date, end: Date, includeUnmonitored = true): Promise<CalendarEvent[]> {
|
||||||
const url = new URL(this.integration.url);
|
const url = this.url("/api/v3/calendar", {
|
||||||
url.pathname = "/api/v3/calendar";
|
start,
|
||||||
url.searchParams.append("start", start.toISOString());
|
end,
|
||||||
url.searchParams.append("end", end.toISOString());
|
unmonitored: includeUnmonitored,
|
||||||
url.searchParams.append("includeSeries", "true");
|
includeSeries: true,
|
||||||
url.searchParams.append("includeEpisodeFile", "true");
|
includeEpisodeFile: true,
|
||||||
url.searchParams.append("includeEpisodeImages", "true");
|
includeEpisodeImages: true,
|
||||||
url.searchParams.append("unmonitored", includeUnmonitored ? "true" : "false");
|
});
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Api-Key": super.getSecretValue("apiKey"),
|
"X-Api-Key": super.getSecretValue("apiKey"),
|
||||||
@@ -47,7 +48,7 @@ export class SonarrIntegration extends MediaOrganizerIntegration {
|
|||||||
private getLinksForSonarCalendarEvent = (event: z.infer<typeof sonarrCalendarEventSchema>) => {
|
private getLinksForSonarCalendarEvent = (event: z.infer<typeof sonarrCalendarEventSchema>) => {
|
||||||
const links: CalendarEvent["links"] = [
|
const links: CalendarEvent["links"] = [
|
||||||
{
|
{
|
||||||
href: `${this.integration.url}/series/${event.series.titleSlug}`,
|
href: this.url(`/series/${event.series.titleSlug}`).toString(),
|
||||||
name: "Sonarr",
|
name: "Sonarr",
|
||||||
logo: "/images/apps/sonarr.svg",
|
logo: "/images/apps/sonarr.svg",
|
||||||
color: undefined,
|
color: undefined,
|
||||||
@@ -92,7 +93,7 @@ export class SonarrIntegration extends MediaOrganizerIntegration {
|
|||||||
public async testConnectionAsync(): Promise<void> {
|
public async testConnectionAsync(): Promise<void> {
|
||||||
await super.handleTestConnectionResponseAsync({
|
await super.handleTestConnectionResponseAsync({
|
||||||
queryFunctionAsync: async () => {
|
queryFunctionAsync: async () => {
|
||||||
return await fetch(`${this.integration.url}/api`, {
|
return await fetch(this.url("/api"), {
|
||||||
headers: { "X-Api-Key": super.getSecretValue("apiKey") },
|
headers: { "X-Api-Key": super.getSecretValue("apiKey") },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export class OpenMediaVaultIntegration extends Integration {
|
|||||||
params: Record<string, unknown>,
|
params: Record<string, unknown>,
|
||||||
headers: Record<string, string> = {},
|
headers: Record<string, string> = {},
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
return await fetch(`${this.integration.url}/rpc.php`, {
|
return await fetch(this.url("/rpc.php"), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { MediaAvailability, MediaRequestStatus } from "../interfaces/media-reque
|
|||||||
*/
|
*/
|
||||||
export class OverseerrIntegration extends Integration implements ISearchableIntegration {
|
export class OverseerrIntegration extends Integration implements ISearchableIntegration {
|
||||||
public async searchAsync(query: string): Promise<{ image?: string; name: string; link: string; text?: string }[]> {
|
public async searchAsync(query: string): Promise<{ image?: string; name: string; link: string; text?: string }[]> {
|
||||||
const response = await fetch(`${this.integration.url}/api/v1/search?query=${query}`, {
|
const response = await fetch(this.url("/api/v1/search", { query }), {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Api-Key": this.getSecretValue("apiKey"),
|
"X-Api-Key": this.getSecretValue("apiKey"),
|
||||||
},
|
},
|
||||||
@@ -24,13 +24,14 @@ export class OverseerrIntegration extends Integration implements ISearchableInte
|
|||||||
|
|
||||||
return schemaData.results.map((result) => ({
|
return schemaData.results.map((result) => ({
|
||||||
name: "name" in result ? result.name : result.title,
|
name: "name" in result ? result.name : result.title,
|
||||||
link: `${this.integration.url}/${result.mediaType}/${result.id}`,
|
link: this.url(`/${result.mediaType}/${result.id}`).toString(),
|
||||||
image: constructSearchResultImage(this.integration.url, result),
|
image: constructSearchResultImage(result),
|
||||||
text: "overview" in result ? result.overview : undefined,
|
text: "overview" in result ? result.overview : undefined,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async testConnectionAsync(): Promise<void> {
|
public async testConnectionAsync(): Promise<void> {
|
||||||
const response = await fetch(`${this.integration.url}/api/v1/auth/me`, {
|
const response = await fetch(this.url("/api/v1/auth/me"), {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Api-Key": this.getSecretValue("apiKey"),
|
"X-Api-Key": this.getSecretValue("apiKey"),
|
||||||
},
|
},
|
||||||
@@ -46,14 +47,14 @@ export class OverseerrIntegration extends Integration implements ISearchableInte
|
|||||||
|
|
||||||
public async getRequestsAsync(): Promise<MediaRequest[]> {
|
public async getRequestsAsync(): Promise<MediaRequest[]> {
|
||||||
//Ensure to get all pending request first
|
//Ensure to get all pending request first
|
||||||
const pendingRequests = await fetch(`${this.integration.url}/api/v1/request?take=-1&filter=pending`, {
|
const pendingRequests = await fetch(this.url("/api/v1/request", { take: -1, filter: "pending" }), {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Api-Key": this.getSecretValue("apiKey"),
|
"X-Api-Key": this.getSecretValue("apiKey"),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
//Change 20 to integration setting (set to -1 for all)
|
//Change 20 to integration setting (set to -1 for all)
|
||||||
const allRequests = await fetch(`${this.integration.url}/api/v1/request?take=20`, {
|
const allRequests = await fetch(this.url("/api/v1/request", { take: 20 }), {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Api-Key": this.getSecretValue("apiKey"),
|
"X-Api-Key": this.getSecretValue("apiKey"),
|
||||||
},
|
},
|
||||||
@@ -83,7 +84,7 @@ export class OverseerrIntegration extends Integration implements ISearchableInte
|
|||||||
availability: request.media.status,
|
availability: request.media.status,
|
||||||
backdropImageUrl: `https://image.tmdb.org/t/p/original/${information.backdropPath}`,
|
backdropImageUrl: `https://image.tmdb.org/t/p/original/${information.backdropPath}`,
|
||||||
posterImagePath: `https://image.tmdb.org/t/p/w600_and_h900_bestv2/${information.posterPath}`,
|
posterImagePath: `https://image.tmdb.org/t/p/w600_and_h900_bestv2/${information.posterPath}`,
|
||||||
href: `${this.integration.url}/${request.type}/${request.media.tmdbId}`,
|
href: this.url(`/${request.type}/${request.media.tmdbId}`).toString(),
|
||||||
type: request.type,
|
type: request.type,
|
||||||
createdAt: request.createdAt,
|
createdAt: request.createdAt,
|
||||||
airDate: new Date(information.airDate),
|
airDate: new Date(information.airDate),
|
||||||
@@ -91,8 +92,8 @@ export class OverseerrIntegration extends Integration implements ISearchableInte
|
|||||||
? ({
|
? ({
|
||||||
...request.requestedBy,
|
...request.requestedBy,
|
||||||
displayName: request.requestedBy.displayName,
|
displayName: request.requestedBy.displayName,
|
||||||
link: `${this.integration.url}/users/${request.requestedBy.id}`,
|
link: this.url(`/users/${request.requestedBy.id}`).toString(),
|
||||||
avatar: constructAvatarUrl(this.integration.url, request.requestedBy.avatar),
|
avatar: this.constructAvatarUrl(request.requestedBy.avatar).toString(),
|
||||||
} satisfies Omit<RequestUser, "requestCount">)
|
} satisfies Omit<RequestUser, "requestCount">)
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
@@ -101,7 +102,7 @@ export class OverseerrIntegration extends Integration implements ISearchableInte
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getStatsAsync(): Promise<RequestStats> {
|
public async getStatsAsync(): Promise<RequestStats> {
|
||||||
const response = await fetch(`${this.integration.url}/api/v1/request/count`, {
|
const response = await fetch(this.url("/api/v1/request/count"), {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Api-Key": this.getSecretValue("apiKey"),
|
"X-Api-Key": this.getSecretValue("apiKey"),
|
||||||
},
|
},
|
||||||
@@ -110,7 +111,7 @@ export class OverseerrIntegration extends Integration implements ISearchableInte
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getUsersAsync(): Promise<RequestUser[]> {
|
public async getUsersAsync(): Promise<RequestUser[]> {
|
||||||
const response = await fetch(`${this.integration.url}/api/v1/user?take=-1`, {
|
const response = await fetch(this.url("/api/v1/user", { take: -1 }), {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Api-Key": this.getSecretValue("apiKey"),
|
"X-Api-Key": this.getSecretValue("apiKey"),
|
||||||
},
|
},
|
||||||
@@ -119,15 +120,15 @@ export class OverseerrIntegration extends Integration implements ISearchableInte
|
|||||||
return users.map((user): RequestUser => {
|
return users.map((user): RequestUser => {
|
||||||
return {
|
return {
|
||||||
...user,
|
...user,
|
||||||
link: `${this.integration.url}/users/${user.id}`,
|
link: this.url(`/users/${user.id}`).toString(),
|
||||||
avatar: constructAvatarUrl(this.integration.url, user.avatar),
|
avatar: this.constructAvatarUrl(user.avatar).toString(),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async approveRequestAsync(requestId: number): Promise<void> {
|
public async approveRequestAsync(requestId: number): Promise<void> {
|
||||||
logger.info(`Approving media request id='${requestId}' integration='${this.integration.name}'`);
|
logger.info(`Approving media request id='${requestId}' integration='${this.integration.name}'`);
|
||||||
await fetch(`${this.integration.url}/api/v1/request/${requestId}/approve`, {
|
await fetch(this.url(`/api/v1/request/${requestId}/approve`), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"X-Api-Key": this.getSecretValue("apiKey"),
|
"X-Api-Key": this.getSecretValue("apiKey"),
|
||||||
@@ -145,7 +146,7 @@ export class OverseerrIntegration extends Integration implements ISearchableInte
|
|||||||
|
|
||||||
public async declineRequestAsync(requestId: number): Promise<void> {
|
public async declineRequestAsync(requestId: number): Promise<void> {
|
||||||
logger.info(`Declining media request id='${requestId}' integration='${this.integration.name}'`);
|
logger.info(`Declining media request id='${requestId}' integration='${this.integration.name}'`);
|
||||||
await fetch(`${this.integration.url}/api/v1/request/${requestId}/decline`, {
|
await fetch(this.url(`/api/v1/request/${requestId}/decline`), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"X-Api-Key": this.getSecretValue("apiKey"),
|
"X-Api-Key": this.getSecretValue("apiKey"),
|
||||||
@@ -162,7 +163,7 @@ export class OverseerrIntegration extends Integration implements ISearchableInte
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getItemInformationAsync(id: number, type: MediaRequest["type"]): Promise<MediaInformation> {
|
private async getItemInformationAsync(id: number, type: MediaRequest["type"]): Promise<MediaInformation> {
|
||||||
const response = await fetch(`${this.integration.url}/api/v1/${type}/${id}`, {
|
const response = await fetch(this.url(`/api/v1/${type}/${id}`), {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Api-Key": this.getSecretValue("apiKey"),
|
"X-Api-Key": this.getSecretValue("apiKey"),
|
||||||
},
|
},
|
||||||
@@ -186,17 +187,17 @@ export class OverseerrIntegration extends Integration implements ISearchableInte
|
|||||||
airDate: movie.releaseDate,
|
airDate: movie.releaseDate,
|
||||||
} satisfies MediaInformation;
|
} satisfies MediaInformation;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const constructAvatarUrl = (appUrl: string, avatar: string) => {
|
private constructAvatarUrl(avatar: string) {
|
||||||
const isAbsolute = avatar.startsWith("http://") || avatar.startsWith("https://");
|
const isAbsolute = avatar.startsWith("http://") || avatar.startsWith("https://");
|
||||||
|
|
||||||
if (isAbsolute) {
|
if (isAbsolute) {
|
||||||
return avatar;
|
return avatar;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.url(`/${avatar}`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return `${appUrl}/${avatar}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface MediaInformation {
|
interface MediaInformation {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -308,11 +309,8 @@ const getUsersSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const constructSearchResultImage = (
|
const constructSearchResultImage = (result: Exclude<z.infer<typeof searchSchema>["results"], undefined>[number]) => {
|
||||||
appUrl: string,
|
const path = getResultImagePath(result);
|
||||||
result: Exclude<z.infer<typeof searchSchema>["results"], undefined>[number],
|
|
||||||
) => {
|
|
||||||
const path = getResultImagePath(appUrl, result);
|
|
||||||
if (!path) {
|
if (!path) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -320,10 +318,7 @@ const constructSearchResultImage = (
|
|||||||
return `https://image.tmdb.org/t/p/w600_and_h900_bestv2${path}`;
|
return `https://image.tmdb.org/t/p/w600_and_h900_bestv2${path}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getResultImagePath = (
|
const getResultImagePath = (result: Exclude<z.infer<typeof searchSchema>["results"], undefined>[number]) => {
|
||||||
appUrl: string,
|
|
||||||
result: Exclude<z.infer<typeof searchSchema>["results"], undefined>[number],
|
|
||||||
) => {
|
|
||||||
switch (result.mediaType) {
|
switch (result.mediaType) {
|
||||||
case "person":
|
case "person":
|
||||||
return result.profilePath;
|
return result.profilePath;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { summaryResponseSchema } from "./pi-hole-types";
|
|||||||
export class PiHoleIntegration extends Integration implements DnsHoleSummaryIntegration {
|
export class PiHoleIntegration extends Integration implements DnsHoleSummaryIntegration {
|
||||||
public async getSummaryAsync(): Promise<DnsHoleSummary> {
|
public async getSummaryAsync(): Promise<DnsHoleSummary> {
|
||||||
const apiKey = super.getSecretValue("apiKey");
|
const apiKey = super.getSecretValue("apiKey");
|
||||||
const response = await fetch(`${this.integration.url}/admin/api.php?summaryRaw&auth=${apiKey}`);
|
const response = await fetch(this.url("/admin/api.php?summaryRaw", { auth: apiKey }));
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to fetch summary for ${this.integration.name} (${this.integration.id}): ${response.statusText}`,
|
`Failed to fetch summary for ${this.integration.name} (${this.integration.id}): ${response.statusText}`,
|
||||||
@@ -36,7 +36,7 @@ export class PiHoleIntegration extends Integration implements DnsHoleSummaryInte
|
|||||||
|
|
||||||
await super.handleTestConnectionResponseAsync({
|
await super.handleTestConnectionResponseAsync({
|
||||||
queryFunctionAsync: async () => {
|
queryFunctionAsync: async () => {
|
||||||
return await fetch(`${this.integration.url}/admin/api.php?status&auth=${apiKey}`);
|
return await fetch(this.url("/admin/api.php?status", { auth: apiKey }));
|
||||||
},
|
},
|
||||||
handleResponseAsync: async (response) => {
|
handleResponseAsync: async (response) => {
|
||||||
try {
|
try {
|
||||||
@@ -53,7 +53,7 @@ export class PiHoleIntegration extends Integration implements DnsHoleSummaryInte
|
|||||||
|
|
||||||
public async enableAsync(): Promise<void> {
|
public async enableAsync(): Promise<void> {
|
||||||
const apiKey = super.getSecretValue("apiKey");
|
const apiKey = super.getSecretValue("apiKey");
|
||||||
const response = await fetch(`${this.integration.url}/admin/api.php?enable&auth=${apiKey}`);
|
const response = await fetch(this.url("/admin/api.php?enable", { auth: apiKey }));
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to enable PiHole for ${this.integration.name} (${this.integration.id}): ${response.statusText}`,
|
`Failed to enable PiHole for ${this.integration.name} (${this.integration.id}): ${response.statusText}`,
|
||||||
@@ -63,7 +63,7 @@ export class PiHoleIntegration extends Integration implements DnsHoleSummaryInte
|
|||||||
|
|
||||||
public async disableAsync(duration?: number): Promise<void> {
|
public async disableAsync(duration?: number): Promise<void> {
|
||||||
const apiKey = super.getSecretValue("apiKey");
|
const apiKey = super.getSecretValue("apiKey");
|
||||||
const url = `${this.integration.url}/admin/api.php?disable${duration ? `=${duration}` : ""}&auth=${apiKey}`;
|
const url = this.url(`/admin/api.php?disable${duration ? `=${duration}` : ""}`, { auth: apiKey });
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export class PlexIntegration extends Integration {
|
|||||||
public async getCurrentSessionsAsync(): Promise<StreamSession[]> {
|
public async getCurrentSessionsAsync(): Promise<StreamSession[]> {
|
||||||
const token = super.getSecretValue("apiKey");
|
const token = super.getSecretValue("apiKey");
|
||||||
|
|
||||||
const response = await fetch(`${this.integration.url}/status/sessions`, {
|
const response = await fetch(this.url("/status/sessions"), {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Plex-Token": token,
|
"X-Plex-Token": token,
|
||||||
},
|
},
|
||||||
@@ -66,7 +66,7 @@ export class PlexIntegration extends Integration {
|
|||||||
|
|
||||||
await super.handleTestConnectionResponseAsync({
|
await super.handleTestConnectionResponseAsync({
|
||||||
queryFunctionAsync: async () => {
|
queryFunctionAsync: async () => {
|
||||||
return await fetch(this.integration.url, {
|
return await fetch(this.url("/"), {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Plex-Token": token,
|
"X-Plex-Token": token,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export class ProwlarrIntegration extends Integration {
|
|||||||
public async getIndexersAsync(): Promise<Indexer[]> {
|
public async getIndexersAsync(): Promise<Indexer[]> {
|
||||||
const apiKey = super.getSecretValue("apiKey");
|
const apiKey = super.getSecretValue("apiKey");
|
||||||
|
|
||||||
const indexerResponse = await fetch(`${this.integration.url}/api/v1/indexer`, {
|
const indexerResponse = await fetch(this.url("/api/v1/indexer"), {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Api-Key": apiKey,
|
"X-Api-Key": apiKey,
|
||||||
},
|
},
|
||||||
@@ -18,7 +18,7 @@ export class ProwlarrIntegration extends Integration {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusResponse = await fetch(`${this.integration.url}/api/v1/indexerstatus`, {
|
const statusResponse = await fetch(this.url("/api/v1/indexerstatus"), {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Api-Key": apiKey,
|
"X-Api-Key": apiKey,
|
||||||
},
|
},
|
||||||
@@ -60,7 +60,7 @@ export class ProwlarrIntegration extends Integration {
|
|||||||
|
|
||||||
public async testAllAsync(): Promise<void> {
|
public async testAllAsync(): Promise<void> {
|
||||||
const apiKey = super.getSecretValue("apiKey");
|
const apiKey = super.getSecretValue("apiKey");
|
||||||
const response = await fetch(`${this.integration.url}/api/v1/indexer/testall`, {
|
const response = await fetch(this.url("/api/v1/indexer/testall"), {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Api-Key": apiKey,
|
"X-Api-Key": apiKey,
|
||||||
},
|
},
|
||||||
@@ -78,7 +78,7 @@ export class ProwlarrIntegration extends Integration {
|
|||||||
|
|
||||||
await super.handleTestConnectionResponseAsync({
|
await super.handleTestConnectionResponseAsync({
|
||||||
queryFunctionAsync: async () => {
|
queryFunctionAsync: async () => {
|
||||||
return await fetch(`${this.integration.url}/api`, {
|
return await fetch(this.url("/api"), {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Api-Key": apiKey,
|
"X-Api-Key": apiKey,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user