diff --git a/docs/docs/configuration/config-file.md b/docs/docs/configuration/config-file.md index 18bf7f1..94389b9 100644 --- a/docs/docs/configuration/config-file.md +++ b/docs/docs/configuration/config-file.md @@ -82,6 +82,8 @@ customFormatDefinitions: ## Configuration Files Reference +If you want to deep dive into available values and parameters you can always check the direct source code reference for available configurations: [Source Code](https://github.com/raydak-labs/configarr/blob/main/src/types/config.types.ts) + ### config.yml The main configuration file that defines your Sonarr and Radarr instances, custom formats, and template includes. @@ -132,6 +134,12 @@ sonarr: - template: sonarr-v4-quality-profile-web-1080p - template: sonarr-v4-custom-formats-web-1080p + # experimental available in all *arr + #media_management: {} + + # experimental available in all *arr + #media_naming: {} + custom_formats: # Custom format assignments - trash_ids: - 47435ece6b99a0b477caf360e79ba0bb # x265 (HD) @@ -205,3 +213,25 @@ RADARR_API_KEY: your_radarr_api_key_here 4. For Kubernetes deployments, create ConfigMaps/Secrets from these files Configarr will automatically load these configurations on startup and apply them to your Sonarr/Radarr instances. + +## Experimental supported fields + +- Experimental support for `media_management` and `media_naming` (since v1.5.0) + With those you can configure different settings in the different tabs available per *arr. + Both fields are under experimental support. + The supports elements in those are dependent on the *arr used. + Check following API documentation of available fields: + + Naming APIs: + + - https://radarr.video/docs/api/#/NamingConfig/get_api_v3_config_naming + - https://sonarr.tv/docs/api/#/NamingConfig/get_api_v3_config_naming + - https://whisparr.com/docs/api/#/NamingConfig/get_api_v3_config_naming + - https://readarr.com/docs/api/#/NamingConfig/get_api_v1_config_naming + + MediaManagement APIs: + + - https://radarr.video/docs/api/#/MediaManagementConfig/get_api_v3_config_mediamanagement + - https://sonarr.tv/docs/api/#/MediaManagementConfig/get_api_v3_config_mediamanagement + - https://whisparr.com/docs/api/#/MediaManagementConfig/get_api_v3_config_mediamanagement + - https://readarr.com/docs/api/#/MediaManagementConfig/get_api_v1_config_mediamanagement diff --git a/examples/full/config/config.yml b/examples/full/config/config.yml index 550ef95..f9c0fad 100644 --- a/examples/full/config/config.yml +++ b/examples/full/config/config.yml @@ -63,6 +63,13 @@ radarr: quality_definition: type: movies + # experimental + media_management: + recycleBin: "/tmp" + + # experimental + media_naming: {} + include: # Comment out any of the following includes to disable them #- template: radarr-quality-definition-movie diff --git a/src/clients/radarr-client.ts b/src/clients/radarr-client.ts index e76e68f..61344d3 100644 --- a/src/clients/radarr-client.ts +++ b/src/clients/radarr-client.ts @@ -87,6 +87,22 @@ export class RadarrClient implements IArrClient; deleteCustomFormat(id: string): Promise; + getNaming(): Promise; + updateNaming(id: string, data: any): Promise; + + getMediamanagement(): Promise; + updateMediamanagement(id: string, data: any): Promise; + getLanguages(): Promise; // System/Health Check @@ -187,6 +193,22 @@ export class UnifiedClient implements IArrClient { return await this.api.deleteCustomFormat(id); } + async getNaming() { + return this.api.getNaming(); + } + + async updateNaming(id: string, data: any) { + return this.api.updateNaming(id, data); + } + + async getMediamanagement() { + return this.api.getMediamanagement(); + } + + async updateMediamanagement(id: string, data: any) { + return this.api.updateMediamanagement(id, data); + } + async getSystemStatus() { return await this.api.getSystemStatus(); } diff --git a/src/clients/whisparr-client.ts b/src/clients/whisparr-client.ts index 1e08187..b049771 100644 --- a/src/clients/whisparr-client.ts +++ b/src/clients/whisparr-client.ts @@ -97,6 +97,22 @@ export class WhisparrClient return this.api.v3CustomformatDelete(+id); } + async getNaming() { + return this.api.v3ConfigNamingList(); + } + + async updateNaming(id: string, data: any) { + return this.api.v3ConfigNamingUpdate(id, data); + } + + async getMediamanagement() { + return this.api.v3ConfigMediamanagementList(); + } + + async updateMediamanagement(id: string, data: any) { + return this.api.v3ConfigMediamanagementUpdate(id, data); + } + // System/Health Check getSystemStatus() { return this.api.v3SystemStatusList(); diff --git a/src/index.ts b/src/index.ts index a043bdf..51966b8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ import { getConfig, validateConfig } from "./config"; import { calculateCFsToManage, loadCustomFormatDefinitions, loadServerCustomFormats, manageCf } from "./custom-formats"; import { loadLocalRecyclarrTemplate } from "./local-importer"; import { logHeading, logger } from "./logger"; +import { calculateMediamanagementDiff, calculateNamingDiff } from "./media-management"; import { calculateQualityDefinitionDiff, loadQualityDefinitionFromServer } from "./quality-definitions"; import { calculateQualityProfilesDiff, filterInvalidQualityProfiles, loadQualityProfilesFromServer } from "./quality-profiles"; import { cloneRecyclarrTemplateRepo, loadRecyclarrTemplates } from "./recyclarr-importer"; @@ -99,6 +100,14 @@ const mergeConfigsAndTemplates = async ( } } + if (template.media_management) { + recylarrMergedTemplates.media_management = { ...recylarrMergedTemplates.media_management, ...template.media_management }; + } + + if (template.media_naming) { + recylarrMergedTemplates.media_naming = { ...recylarrMergedTemplates.media_naming, ...template.media_naming }; + } + // TODO Ignore recursive include for now if (template.include) { logger.warn(`Recursive includes not supported at the moment. Ignoring.`); @@ -127,6 +136,14 @@ const mergeConfigsAndTemplates = async ( recylarrMergedTemplates.quality_profiles.push(...value.quality_profiles); } + if (value.media_management) { + recylarrMergedTemplates.media_management = { ...recylarrMergedTemplates.media_management, ...value.media_management }; + } + + if (value.media_naming) { + recylarrMergedTemplates.media_naming = { ...recylarrMergedTemplates.media_naming, ...value.media_naming }; + } + const recyclarrProfilesMerged = recylarrMergedTemplates.quality_profiles.reduce>((p, c) => { const profile = p.get(c.name); @@ -270,6 +287,30 @@ const pipeline = async (value: InputConfigArrInstance, arrType: ArrType) => { } } + const namingDiff = await calculateNamingDiff(config.media_naming); + + if (namingDiff) { + if (getEnvs().DRY_RUN) { + logger.info("DryRun: Would update MediaNaming."); + } else { + // TODO this will need a radarr/sonarr separation for sure to have good and correct typings + await api.updateNaming(namingDiff.updatedData.id! + "", namingDiff.updatedData as any); // Ignore types + logger.info(`Updated MediaNaming`); + } + } + + const managementDiff = await calculateMediamanagementDiff(config.media_management); + + if (managementDiff) { + if (getEnvs().DRY_RUN) { + logger.info("DryRun: Would update MediaManagement."); + } else { + // TODO this will need a radarr/sonarr separation for sure to have good and correct typings + await api.updateMediamanagement(managementDiff.updatedData.id! + "", managementDiff.updatedData as any); // Ignore types + logger.info(`Updated MediaManagement`); + } + } + // calculate diff from server <-> what we want to be there const serverQP = await loadQualityProfilesFromServer(); const { changedQPs, create, noChanges } = await calculateQualityProfilesDiff(mergedCFs, config, serverQP, serverQD, serverCFs); diff --git a/src/media-management.ts b/src/media-management.ts new file mode 100644 index 0000000..faf218c --- /dev/null +++ b/src/media-management.ts @@ -0,0 +1,73 @@ +import { getUnifiedClient } from "./clients/unified-client"; +import { logger } from "./logger"; +import { MediaManagementType, MediaNamingType } from "./types/config.types"; +import { compareMediamanagement, compareNaming } from "./util"; + +const loadNamingFromServer = async () => { + const api = getUnifiedClient(); + const result = await api.getNaming(); + return result; +}; + +const loadMediamanagementConfigFromServer = async () => { + const api = getUnifiedClient(); + const result = await api.getMediamanagement(); + return result; +}; + +export const calculateNamingDiff = async (mediaNaming?: MediaNamingType) => { + if (mediaNaming == null) { + logger.debug(`Config 'media_naming' not specified. Ignoring.`); + return null; + } + + const serverData = await loadNamingFromServer(); + + const { changes, equal } = compareNaming(serverData, mediaNaming); + + if (equal) { + logger.debug(`Media naming settings are in sync`); + return null; + } + + logger.info(`Found ${changes.length} differences for media naming.`); + logger.debug(changes, `Found following changes for media naming`); + + return { + changes, + updatedData: { + ...serverData, + ...mediaNaming, + }, + }; +}; + +export const calculateMediamanagementDiff = async (mediaManagement?: MediaManagementType) => { + if (mediaManagement == null) { + logger.debug(`Config 'media_management' not specified. Ignoring.`); + return null; + } + + const serverData = await loadMediamanagementConfigFromServer(); + + console.log(serverData, mediaManagement); + logger.debug(serverData, "Media Server"); + logger.debug(mediaManagement, "Media Local"); + const { changes, equal } = compareMediamanagement(serverData, mediaManagement); + + if (equal) { + logger.debug(`Media management settings are in sync`); + return null; + } + + logger.info(`Found ${changes.length} differences for media management.`); + logger.debug(changes, `Found following changes for media management`); + + return { + changes, + updatedData: { + ...serverData, + ...mediaManagement, + }, + }; +}; diff --git a/src/types/common.types.ts b/src/types/common.types.ts index 58eb30b..ef8596c 100644 --- a/src/types/common.types.ts +++ b/src/types/common.types.ts @@ -63,7 +63,16 @@ export type CFProcessing = { }; export type MappedTemplates = Partial< - Pick + Pick< + InputConfigArrInstance, + | "quality_definition" + | "custom_formats" + | "include" + | "quality_profiles" + | "customFormatDefinitions" + | "media_management" + | "media_naming" + > >; export type MappedMergedTemplates = MappedTemplates & Required>; diff --git a/src/types/config.types.ts b/src/types/config.types.ts index e522e2f..2e2f300 100644 --- a/src/types/config.types.ts +++ b/src/types/config.types.ts @@ -38,8 +38,22 @@ export type InputConfigArrInstance = { custom_formats?: InputConfigCustomFormat[]; // TODO this is not correct. The profile can be added partly -> InputConfigQualityProfile quality_profiles: ConfigQualityProfile[]; + /* @experimental */ + media_management?: MediaManagementType; + /* @experimental */ + media_naming?: MediaNamingType; } & Pick; +// HINT: Experimental +export type MediaManagementType = { + // APIs not consistent across different *arrs. Keeping empty or generic +}; + +// HINT: Experimental +export type MediaNamingType = { + // APIs not consistent across different *arrs. Keeping empty or generic +}; + export type InputConfigQualityProfile = { name: string; reset_unmatched_scores?: { diff --git a/src/util.ts b/src/util.ts index 6e05b8a..4ff32a9 100644 --- a/src/util.ts +++ b/src/util.ts @@ -113,6 +113,14 @@ export function compareCustomFormats( return compareObjectsCarr(serverObject, localObject); } +export function compareNaming(serverObject: any, localObject: any): ReturnType { + return compareObjectsCarr(serverObject, localObject); +} + +export function compareMediamanagement(serverObject: any, localObject: any): ReturnType { + return compareObjectsCarr(serverObject, localObject); +} + function compareObjectsCarr(serverObject: any, localObject: any, parent?: string): { equal: boolean; changes: string[] } { const changes: string[] = [];