Skip to content

Commit

Permalink
feat: add configuration options for media management tab
Browse files Browse the repository at this point in the history
  • Loading branch information
BlackDark committed Dec 17, 2024
1 parent 4d613e5 commit 3883a0a
Show file tree
Hide file tree
Showing 12 changed files with 267 additions and 1 deletion.
28 changes: 28 additions & 0 deletions docs/docs/configuration/config-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -205,3 +213,23 @@ 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
7 changes: 7 additions & 0 deletions examples/full/config/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions src/clients/radarr-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,22 @@ export class RadarrClient implements IArrClient<QualityProfileResource, QualityD
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();
Expand Down
16 changes: 16 additions & 0 deletions src/clients/readarr-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,22 @@ export class ReadarrClient
return this.api.v1MetadataprofileUpdate(id.toString(), profile);
}

async getNaming() {
return this.api.v1ConfigNamingList();
}

async updateNaming(id: string, data: any) {
return this.api.v1ConfigNamingUpdate(id, data);
}

async getMediamanagement() {
return this.api.v1ConfigMediamanagementList();
}

async updateMediamanagement(id: string, data: any) {
return this.api.v1ConfigMediamanagementUpdate(id, data);
}

// System/Health Check
getSystemStatus() {
return this.api.v1SystemStatusList();
Expand Down
16 changes: 16 additions & 0 deletions src/clients/sonarr-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,22 @@ export class SonarrClient implements IArrClient<QualityProfileResource, QualityD
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();
Expand Down
22 changes: 22 additions & 0 deletions src/clients/unified-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@ export interface IArrClient<
updateCustomFormat(id: string, format: CF): Promise<CF>;
deleteCustomFormat(id: string): Promise<void>;

getNaming(): Promise<any>;
updateNaming(id: string, data: any): Promise<any>;

getMediamanagement(): Promise<any>;
updateMediamanagement(id: string, data: any): Promise<any>;

getLanguages(): Promise<L[]>;

// System/Health Check
Expand Down Expand Up @@ -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();
}
Expand Down
16 changes: 16 additions & 0 deletions src/clients/whisparr-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
41 changes: 41 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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.`);
Expand Down Expand Up @@ -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<Map<string, ConfigQualityProfile>>((p, c) => {
const profile = p.get(c.name);

Expand Down Expand Up @@ -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);
Expand Down
73 changes: 73 additions & 0 deletions src/media-management.ts
Original file line number Diff line number Diff line change
@@ -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,
},
};
};
11 changes: 10 additions & 1 deletion src/types/common.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,16 @@ export type CFProcessing = {
};

export type MappedTemplates = Partial<
Pick<InputConfigArrInstance, "quality_definition" | "custom_formats" | "include" | "quality_profiles" | "customFormatDefinitions">
Pick<
InputConfigArrInstance,
| "quality_definition"
| "custom_formats"
| "include"
| "quality_profiles"
| "customFormatDefinitions"
| "media_management"
| "media_naming"
>
>;

export type MappedMergedTemplates = MappedTemplates & Required<Pick<MappedTemplates, "custom_formats" | "quality_profiles">>;
Expand Down
14 changes: 14 additions & 0 deletions src/types/config.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<InputConfigSchema, "customFormatDefinitions">;

// 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?: {
Expand Down
8 changes: 8 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,14 @@ export function compareCustomFormats(
return compareObjectsCarr(serverObject, localObject);
}

export function compareNaming(serverObject: any, localObject: any): ReturnType<typeof compareObjectsCarr> {
return compareObjectsCarr(serverObject, localObject);
}

export function compareMediamanagement(serverObject: any, localObject: any): ReturnType<typeof compareObjectsCarr> {
return compareObjectsCarr(serverObject, localObject);
}

function compareObjectsCarr(serverObject: any, localObject: any, parent?: string): { equal: boolean; changes: string[] } {
const changes: string[] = [];

Expand Down

0 comments on commit 3883a0a

Please sign in to comment.