Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add experimental config options for media management tab #6

Merged
merged 1 commit into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 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,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
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
Loading