diff --git a/docs/docs/configuration/_include/config-file-sample.yml b/docs/docs/configuration/_include/config-file-sample.yml index 93ba5d8..5cc5602 100644 --- a/docs/docs/configuration/_include/config-file-sample.yml +++ b/docs/docs/configuration/_include/config-file-sample.yml @@ -36,6 +36,9 @@ sonarr: api_key: !secret SONARR_API_KEY # Reference to API key in secrets.yml # api_key: !env SONARR_API_KEY # load from environment variable + # since v1.11.0. You can disable instances. Optional + # enabled: false + quality_definition: type: series # Quality definition type for Sonarr diff --git a/docs/docs/configuration/config-file.md b/docs/docs/configuration/config-file.md index 74ba92b..9a3ec4d 100644 --- a/docs/docs/configuration/config-file.md +++ b/docs/docs/configuration/config-file.md @@ -118,6 +118,12 @@ radarrEnabled: false whisparrEnabled: false readarrEnabled: false lidarrEnabled: false + +# You can also disable on per instance basis +sonarr: + instance1: + # ... + enabled: false ``` ## Quality Definition / Size diff --git a/docs/docs/configuration/environment-variables.md b/docs/docs/configuration/environment-variables.md index cebbf76..6bc8380 100644 --- a/docs/docs/configuration/environment-variables.md +++ b/docs/docs/configuration/environment-variables.md @@ -15,6 +15,7 @@ Each variable can be set to customize the behavior of the application. | Variable Name | Default Value | Required | Description | | -------------------- | ------------------------- | -------- | ------------------------------------------------------------------------------------------- | | `LOG_LEVEL` | `"info"` | No | Sets the logging level. Options are `trace`, `debug`, `info`, `warn`, `error`, and `fatal`. | +| `LOG_STACKTRACE` | `"false"` | No | (Experimental, v1.11.0) Outputs additionally stacktraces of underlying errors. | | `CONFIG_LOCATION` | `"./config/config.yml"` | No | Specifies the path to the configuration file. | | `SECRETS_LOCATION` | `"./config/secrets.yml"` | No | Specifies the path to the secrets file. | | `CUSTOM_REPO_ROOT` | `"./repos"` | No | Defines the root directory for custom repositories. | @@ -23,6 +24,7 @@ Each variable can be set to customize the behavior of the application. | `LOAD_LOCAL_SAMPLES` | `"false"` | No | If `"true"`, loads local sample data for testing purposes. | | `DEBUG_CREATE_FILES` | `"false"` | No | Enables debugging for file creation processes when set to `"true"`. | | `TZ` | `"Etc/UTC"` | No | Timezone for the container. | +| `STOP_ON_ERROR` | `"false"` | No | (Experimental, v1.11.0) Stop execution on any error on any instance. | ## Usage diff --git a/examples/full/config/config.yml b/examples/full/config/config.yml index 1ff1b3d..488b82f 100644 --- a/examples/full/config/config.yml +++ b/examples/full/config/config.yml @@ -43,6 +43,9 @@ sonarr: base_url: http://sonarr:8989 api_key: !secret SONARR_API_KEY + # since v1.11.0, optional, for disabling instances + enabled: false + quality_definition: type: series diff --git a/src/__generated__/ky-client.ts b/src/__generated__/ky-client.ts index 5800c45..a770330 100644 --- a/src/__generated__/ky-client.ts +++ b/src/__generated__/ky-client.ts @@ -190,27 +190,47 @@ export class HttpClient { logger.debug(`Error during request with error: ${error?.name}`); if (error instanceof HTTPError) { - if (error.response) { + const { response, request } = error; + + if (response) { // The request was made and the server responded with a status code // that falls out of the range of 2xx - const errorJson = await error.response.json(); - logger.error(errorJson, `Failed executing request: ${error.message}`); - throw new Error(errorJson); - } else if (error.request) { + const contentType = response.headers.get("content-type"); + + if (contentType && contentType.includes("application/json")) { + // Process JSON data + const errorJson = await response.json(); + logger.error(errorJson, `Failed executing request: ${error.message}`); + throw new Error(errorJson); + } else { + // Handle non-JSON response + logger.error(`HTTP Error: ${response.status} ${response.statusText}`); + } + } else if (request) { // The request was made but no response was received - // `error.request` is an instance of XMLHttpRequest in the browser and an instance of + // `request` is an instance of XMLHttpRequest in the browser and an instance of // http.ClientRequest in node.js - const errorJson = await error.request.json(); + const errorJson = await request.json(); logger.error(errorJson, `Failed during request (probably some connection issues?)`); throw error; } else { // Something happened in setting up the request that triggered an Error - logger.error(error, `No request/response information. Unknown error`); + logger.error(`No request/response information. Unknown error`); } } else if (error instanceof TypeError) { - logger.error(error, `Probably some connection issues. If not, feel free to open an issue with details to improve handling.`); + let errorMessage = "Probably some connection issues. If not, feel free to open an issue with details to improve handling."; + + if (error.cause && error.cause instanceof Error) { + errorMessage += ` Caused by: '${error.cause.message}'.`; + + if ("code" in error.cause) { + errorMessage += ` Error code: '${error.cause.code}'.`; + } + } + + logger.error(errorMessage); } else { - logger.error(error, `An not expected error happened. Feel free to open an issue with details to improve handling.`); + logger.error(`An not expected error happened. Feel free to open an issue with details to improve handling.`); } throw error; diff --git a/src/clients/lidarr-client.ts b/src/clients/lidarr-client.ts index 56abff7..dc508fb 100644 --- a/src/clients/lidarr-client.ts +++ b/src/clients/lidarr-client.ts @@ -8,7 +8,7 @@ import { QualityProfileResource, } from "../__generated__/lidarr/data-contracts"; import { logger } from "../logger"; -import { IArrClient, validateClientParams } from "./unified-client"; +import { IArrClient, logConnectionError, validateClientParams } from "./unified-client"; export class LidarrClient implements IArrClient { private api!: Api; @@ -112,7 +112,9 @@ export class LidarrClient implements IArrClient { private api!: Api; @@ -112,7 +112,8 @@ export class RadarrClient implements IArrClient @@ -114,7 +114,8 @@ export class ReadarrClient try { await this.api.v1HealthList(); } catch (error) { - logger.error(error); + const message = logConnectionError(error, "READARR"); + logger.error(message); return false; } diff --git a/src/clients/sonarr-client.ts b/src/clients/sonarr-client.ts index 1db3bed..1631596 100644 --- a/src/clients/sonarr-client.ts +++ b/src/clients/sonarr-client.ts @@ -7,7 +7,7 @@ import { QualityProfileResource, } from "../__generated__/sonarr/data-contracts"; import { logger } from "../logger"; -import { IArrClient, validateClientParams } from "./unified-client"; +import { IArrClient, logConnectionError, validateClientParams } from "./unified-client"; export type SonarrQualityProfileResource = { id?: number; @@ -104,7 +104,8 @@ export class SonarrClient implements IArrClient { +export const logConnectionError = (error: any, arrType: ArrType) => { let message; const arrLabel = arrType.toLowerCase(); const causeError = error?.cause?.message || error?.cause?.errors?.map((e: any) => e.message).join(";") || undefined; @@ -43,8 +43,6 @@ export const handleErrorApi = (error: any, arrType: ArrType) => { const errorMessage = (error.message && `Message: ${error.message}`) || ""; const causeMessage = (causeError && `- Cause: ${causeError}`) || ""; - logger.error(`Error configuring ${arrLabel} API. ${errorMessage} ${causeMessage}`); - if (error.response) { // The request was made and the server responded with a status code // that falls out of the range of 2xx @@ -54,18 +52,24 @@ export const handleErrorApi = (error: any, arrType: ArrType) => { message = `An unexpected error occurred while setting up the ${arrLabel} request: ${errorMessage} ${causeMessage}. Please try again.`; } - throw new Error(message); + return message; }; export const configureApi = async (type: ArrType, baseUrl: string, apiKey: string) => { unsetApi(); unifiedClient = new UnifiedClient(type, baseUrl, apiKey); + let connectionSuccessful = false; try { - await unifiedClient.testConnection(); + connectionSuccessful = await unifiedClient.testConnection(); } catch (error: any) { - handleErrorApi(error, type); + logger.error(`Unhandled connection error.`); + throw error; + } + + if (!connectionSuccessful) { + throw new Error(`Could not connect to client: ${type} - ${baseUrl}`); } return unifiedClient; diff --git a/src/clients/whisparr-client.ts b/src/clients/whisparr-client.ts index b049771..bc80ed0 100644 --- a/src/clients/whisparr-client.ts +++ b/src/clients/whisparr-client.ts @@ -8,7 +8,7 @@ import { } from "../__generated__/whisparr/data-contracts"; import { logger } from "../logger"; import { cloneWithJSON } from "../util"; -import { IArrClient, validateClientParams } from "./unified-client"; +import { IArrClient, logConnectionError, validateClientParams } from "./unified-client"; /** * Overwrite wrong types for now @@ -122,7 +122,8 @@ export class WhisparrClient try { await this.api.v3HealthList(); } catch (error) { - logger.error(error); + const message = logConnectionError(error, "WHISPARR"); + logger.error(message); return false; } diff --git a/src/env.ts b/src/env.ts index f8d744c..1162054 100644 --- a/src/env.ts +++ b/src/env.ts @@ -32,6 +32,18 @@ const schema = z.object({ .transform((x) => x === "true") .pipe(z.boolean()) .default("false"), + STOP_ON_ERROR: z + .string() + .toLowerCase() + .transform((x) => x === "true") + .pipe(z.boolean()) + .default("false"), + LOG_STACKTRACE: z + .string() + .toLowerCase() + .transform((x) => x === "true") + .pipe(z.boolean()) + .default("false"), }); // declare global { diff --git a/src/index.ts b/src/index.ts index 4824626..244a38d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,7 +9,7 @@ import { ServerCache } from "./cache"; import { configureApi, getUnifiedClient, unsetApi } from "./clients/unified-client"; import { getConfig, mergeConfigsAndTemplates } from "./config"; import { calculateCFsToManage, loadCustomFormatDefinitions, loadServerCustomFormats, manageCf } from "./custom-formats"; -import { logHeading, logger } from "./logger"; +import { logHeading, logInstanceHeading, logger } from "./logger"; import { calculateMediamanagementDiff, calculateNamingDiff } from "./media-management"; import { calculateQualityDefinitionDiff, loadQualityDefinitionFromServer } from "./quality-definitions"; import { calculateQualityProfilesDiff, loadQualityProfilesFromServer } from "./quality-profiles"; @@ -177,18 +177,48 @@ const runArrType = async ( globalConfig: InputConfigSchema, arrEntry: Record | undefined, ) => { + const status = { + success: 0, + failure: 0, + skipped: 0, + }; + if (arrEntry == null || Array.isArray(arrEntry) || typeof arrEntry !== "object" || Object.keys(arrEntry).length <= 0) { logHeading(`No ${arrType} instances defined.`); } else { logHeading(`Processing ${arrType} ...`); for (const [instanceName, instance] of Object.entries(arrEntry)) { - logger.info(`Processing ${arrType} Instance: ${instanceName}`); - await configureApi(arrType, instance.base_url, instance.api_key); - await pipeline(globalConfig, instance, arrType); - unsetApi(); + logInstanceHeading(`Processing ${arrType} Instance: ${instanceName} ...`); + + if (instance.enabled === false) { + logger.info(`Instance ${arrType} - ${instanceName} is disabled!`); + status.skipped++; + continue; + } + + try { + await configureApi(arrType, instance.base_url, instance.api_key); + await pipeline(globalConfig, instance, arrType); + status.success++; + } catch (err: unknown) { + logger.error(`Failure during configuring: ${arrType} - ${instanceName}`); + status.failure++; + if (getEnvs().LOG_STACKTRACE) { + logger.error(err); + } + if (getEnvs().STOP_ON_ERROR) { + throw new Error(`Stopping further execution because 'STOP_ON_ERROR' is enabled.`); + } + } finally { + unsetApi(); + } + + logger.info(""); } } + + return status; }; const run = async () => { @@ -203,40 +233,50 @@ const run = async () => { // TODO currently this has to be run sequentially because of the centrally configured api + const totalStatus: string[] = []; + // Sonarr if (globalConfig.sonarrEnabled == null || globalConfig.sonarrEnabled) { - await runArrType("SONARR", globalConfig, globalConfig.sonarr); + const result = await runArrType("SONARR", globalConfig, globalConfig.sonarr); + totalStatus.push(`SONARR: (${result.success}/${result.failure}/${result.skipped})`); } else { logger.debug(`Sonarr disabled in config`); } // Radarr if (globalConfig.radarrEnabled == null || globalConfig.radarrEnabled) { - await runArrType("RADARR", globalConfig, globalConfig.radarr); + const result = await runArrType("RADARR", globalConfig, globalConfig.radarr); + totalStatus.push(`RADARR: (${result.success}/${result.failure}/${result.skipped})`); } else { logger.debug(`Radarr disabled in config`); } // Whisparr if (globalConfig.whisparrEnabled == null || globalConfig.whisparrEnabled) { - await runArrType("WHISPARR", globalConfig, globalConfig.whisparr); + const result = await runArrType("WHISPARR", globalConfig, globalConfig.whisparr); + totalStatus.push(`WHISPARR: (${result.success}/${result.failure}/${result.skipped})`); } else { logger.debug(`Whisparr disabled in config`); } // Readarr if (globalConfig.readarrEnabled == null || globalConfig.readarrEnabled) { - await runArrType("READARR", globalConfig, globalConfig.readarr); + const result = await runArrType("READARR", globalConfig, globalConfig.readarr); + totalStatus.push(`READARR: (${result.success}/${result.failure}/${result.skipped})`); } else { logger.debug(`Readarr disabled in config`); } // Lidarr if (globalConfig.lidarrEnabled == null || globalConfig.lidarrEnabled) { - await runArrType("LIDARR", globalConfig, globalConfig.lidarr); + const result = await runArrType("LIDARR", globalConfig, globalConfig.lidarr); + totalStatus.push(`LIDARR: (${result.success}/${result.failure}/${result.skipped})`); } else { logger.debug(`Lidarr disabled in config`); } + + logger.info(``); + logger.info(`Execution Summary (success/failure/skipped) instances: ${totalStatus.join(" - ")}`); }; run(); diff --git a/src/logger.ts b/src/logger.ts index 3650e49..7147943 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -53,5 +53,9 @@ export const logHeading = (title: string) => { logger.info(""); }; +export const logInstanceHeading = (title: string) => { + logger.info(`### ${title}`); +}; + // For debugging how envs have been loaded logger.debug({ envs: getEnvs(), helpers: getHelpers() }, `Loaded following configuration from ENVs and mapped`); diff --git a/src/types/config.types.ts b/src/types/config.types.ts index 9199429..928aa8b 100644 --- a/src/types/config.types.ts +++ b/src/types/config.types.ts @@ -40,6 +40,10 @@ export type InputConfigCustomFormat = { export type InputConfigArrInstance = { base_url: string; api_key: string; + /** + * since v1.11.0 + */ + enabled?: boolean; quality_definition?: { type?: string; preferred_ratio?: number; // 0.0 - 1.0