Skip to content

Commit 2aa101c

Browse files
committed
refactor: split config merging and improve types
1 parent 98aa2fe commit 2aa101c

14 files changed

+208
-177
lines changed

index.ts

+57-46
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import "dotenv/config";
33
import fs from "node:fs";
44
import { MergedCustomFormatResource } from "./src/__generated__/mergedTypes";
55
import { configureRadarrApi, configureSonarrApi, getArrApi, unsetApi } from "./src/api";
6-
import { getConfig } from "./src/config";
6+
import { getConfig, validateConfig } from "./src/config";
77
import {
88
calculateCFsToManage,
99
loadCFFromConfig,
@@ -29,13 +29,21 @@ import {
2929
transformTrashQPCFs,
3030
transformTrashQPToTemplate,
3131
} from "./src/trash-guide";
32-
import { ArrType, MappedMergedTemplates } from "./src/types/common.types";
33-
import { ConfigArrInstance, ConfigQualityProfile, YamlConfigIncludeItem } from "./src/types/config.types";
32+
import { ArrType, CFProcessing, MappedMergedTemplates } from "./src/types/common.types";
33+
import { ConfigQualityProfile, InputConfigArrInstance, InputConfigIncludeItem, MergedConfigInstance } from "./src/types/config.types";
3434
import { TrashQualityDefintion } from "./src/types/trashguide.types";
3535
import { DEBUG_CREATE_FILES, IS_DRY_RUN } from "./src/util";
3636

37-
const pipeline = async (value: ConfigArrInstance, arrType: ArrType) => {
38-
const api = getArrApi();
37+
/**
38+
* Load data from trash, recyclarr, custom configs and merge.
39+
* Afterwards do sanitize and check against required configuration.
40+
* @param value
41+
* @param arrType
42+
*/
43+
const mergeConfigsAndTemplates = async (
44+
value: InputConfigArrInstance,
45+
arrType: ArrType,
46+
): Promise<{ mergedCFs: CFProcessing; config: MergedConfigInstance }> => {
3947
const recyclarrTemplateMap = loadRecyclarrTemplates(arrType);
4048
const trashTemplates = await loadQPFromTrash(arrType);
4149

@@ -45,9 +53,7 @@ const pipeline = async (value: ConfigArrInstance, arrType: ArrType) => {
4553
};
4654

4755
if (value.include) {
48-
logger.info(`Found ${value.include.length} templates to include ...`);
49-
50-
const mappedIncludes = value.include.reduce<{ recyclarr: YamlConfigIncludeItem[]; trash: YamlConfigIncludeItem[] }>(
56+
const mappedIncludes = value.include.reduce<{ recyclarr: InputConfigIncludeItem[]; trash: InputConfigIncludeItem[] }>(
5157
(previous, current) => {
5258
switch (current.source) {
5359
case "TRASH":
@@ -65,14 +71,15 @@ const pipeline = async (value: ConfigArrInstance, arrType: ArrType) => {
6571
{ recyclarr: [], trash: [] },
6672
);
6773

68-
logger.debug(mappedIncludes.recyclarr, `Included ${mappedIncludes.recyclarr.length} templates [recyclarr]`);
69-
logger.debug(mappedIncludes.trash, `Included ${mappedIncludes.trash.length} templates [trash]`);
74+
logger.info(
75+
`Found ${value.include.length} templates to include: [recyclarr]=${mappedIncludes.recyclarr.length}, [trash]=${mappedIncludes.trash.length} ...`,
76+
);
7077

7178
mappedIncludes.recyclarr.forEach((e) => {
7279
const template = recyclarrTemplateMap.get(e.template);
7380

7481
if (!template) {
75-
logger.info(`Unknown recyclarr template requested: ${e.template}`);
82+
logger.warn(`Unknown recyclarr template requested: ${e.template}`);
7683
return;
7784
}
7885

@@ -97,7 +104,7 @@ const pipeline = async (value: ConfigArrInstance, arrType: ArrType) => {
97104
const template = trashTemplates.get(e.template);
98105

99106
if (!template) {
100-
logger.info(`Unknown trash template requested: ${e.template}`);
107+
logger.warn(`Unknown trash template requested: ${e.template}`);
101108
return;
102109
}
103110

@@ -147,10 +154,21 @@ const pipeline = async (value: ConfigArrInstance, arrType: ArrType) => {
147154
const configCFDefinition = loadCFFromConfig();
148155
const mergedCFs = mergeCfSources([trashCFs, localFileCFs, configCFDefinition]);
149156

150-
const idsToManage = calculateCFsToManage(recylarrMergedTemplates);
157+
const validatedConfig = validateConfig(recylarrMergedTemplates);
158+
return { mergedCFs: mergedCFs, config: validatedConfig };
159+
};
160+
161+
const pipeline = async (value: InputConfigArrInstance, arrType: ArrType) => {
162+
const api = getArrApi();
163+
164+
const { config, mergedCFs } = await mergeConfigsAndTemplates(value, arrType);
165+
166+
const idsToManage = calculateCFsToManage(config);
151167

152168
logger.debug(Array.from(idsToManage), `CustomFormats to manage`);
153169

170+
// TODO here the configs should be merged -> sanitize + check
171+
154172
const serverCFs = await loadServerCustomFormats();
155173
logger.info(`CustomFormats on server: ${serverCFs.length}`);
156174

@@ -162,7 +180,7 @@ const pipeline = async (value: ConfigArrInstance, arrType: ArrType) => {
162180
await manageCf(mergedCFs, serverCFMapping, idsToManage);
163181
logger.info(`CustomFormats synchronized`);
164182

165-
const qualityDefinition = recylarrMergedTemplates.quality_definition?.type;
183+
const qualityDefinition = config.quality_definition?.type;
166184

167185
if (qualityDefinition) {
168186
const qdSonarr = await loadQualityDefinitionFromServer();
@@ -203,10 +221,11 @@ const pipeline = async (value: ConfigArrInstance, arrType: ArrType) => {
203221

204222
// merge CFs of templates and custom CFs into one mapping of QualityProfile -> CFs + Score
205223
// TODO traversing the merged templates probably to often once should be enough. Loop once and extract a couple of different maps, arrays as needed. Future optimization.
206-
const cfToQualityProfiles = mapQualityProfiles(mergedCFs, recylarrMergedTemplates.custom_formats, recylarrMergedTemplates);
224+
// TODO double config
225+
const cfToQualityProfiles = mapQualityProfiles(mergedCFs, config.custom_formats, config);
207226

208227
// merge profiles from recyclarr templates into one
209-
const qualityProfilesMerged = recylarrMergedTemplates.quality_profiles.reduce((p, c) => {
228+
const qualityProfilesMerged = config.quality_profiles.reduce((p, c) => {
210229
let existingQp = p.get(c.name);
211230

212231
if (!existingQp) {
@@ -289,42 +308,34 @@ const run = async () => {
289308

290309
// TODO currently this has to be run sequentially because of the centrally configured api
291310

292-
logHeading(`Processing Sonarr ...`);
311+
const sonarrConfig = applicationConfig.sonarr;
293312

294-
for (const instanceName in applicationConfig.sonarr) {
295-
const instance = applicationConfig.sonarr[instanceName];
296-
logger.info(`Processing Sonarr Instance: ${instanceName}`);
297-
await configureSonarrApi(instance.base_url, instance.api_key);
298-
await pipeline(instance, "SONARR");
299-
unsetApi();
300-
}
313+
if (sonarrConfig == null || Array.isArray(sonarrConfig) || typeof sonarrConfig !== "object" || Object.keys(sonarrConfig).length <= 0) {
314+
logHeading(`No Sonarr instances defined.`);
315+
} else {
316+
logHeading(`Processing Sonarr ...`);
301317

302-
if (
303-
typeof applicationConfig.sonarr === "object" &&
304-
!Array.isArray(applicationConfig.sonarr) &&
305-
applicationConfig.sonarr !== null &&
306-
Object.keys(applicationConfig.sonarr).length <= 0
307-
) {
308-
logger.info(`No sonarr instances defined.`);
318+
for (const [instanceName, instance] of Object.entries(sonarrConfig)) {
319+
logger.info(`Processing Sonarr Instance: ${instanceName}`);
320+
await configureSonarrApi(instance.base_url, instance.api_key);
321+
await pipeline(instance, "SONARR");
322+
unsetApi();
323+
}
309324
}
310325

311-
logHeading(`Processing Radarr ...`);
326+
const radarrConfig = applicationConfig.radarr;
312327

313-
for (const instanceName in applicationConfig.radarr) {
314-
logger.info(`Processing Radarr instance: ${instanceName}`);
315-
const instance = applicationConfig.radarr[instanceName];
316-
await configureRadarrApi(instance.base_url, instance.api_key);
317-
await pipeline(instance, "RADARR");
318-
unsetApi();
319-
}
328+
if (radarrConfig == null || Array.isArray(radarrConfig) || typeof radarrConfig !== "object" || Object.keys(radarrConfig).length <= 0) {
329+
logHeading(`No Radarr instances defined.`);
330+
} else {
331+
logHeading(`Processing Radarr ...`);
320332

321-
if (
322-
typeof applicationConfig.radarr === "object" &&
323-
!Array.isArray(applicationConfig.radarr) &&
324-
applicationConfig.radarr !== null &&
325-
Object.keys(applicationConfig.radarr).length <= 0
326-
) {
327-
logger.info(`No radarr instances defined.`);
333+
for (const [instanceName, instance] of Object.entries(radarrConfig)) {
334+
logger.info(`Processing Radarr Instance: ${instanceName}`);
335+
await configureRadarrApi(instance.base_url, instance.api_key);
336+
await pipeline(instance, "RADARR");
337+
unsetApi();
338+
}
328339
}
329340
};
330341

src/__generated__/mergedTypes.ts

+7-7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/config.test.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@ radarr: {}
4343

4444
test("should transform without error - quality_profiles instead of assign_scores_to", async () => {
4545
const cloned = cloneWithJSON(config);
46-
const instance = cloned.sonarr["instance1"];
47-
const { assign_scores_to, quality_profiles, ...rest } = instance.custom_formats[0];
46+
const instance = cloned.sonarr!["instance1"];
47+
const { assign_scores_to, quality_profiles, ...rest } = instance.custom_formats![0];
4848

49-
instance.custom_formats[0] = { ...rest, quality_profiles: assign_scores_to ?? quality_profiles };
49+
instance.custom_formats![0] = { ...rest, quality_profiles: assign_scores_to ?? quality_profiles };
5050

5151
const transformed = transformConfig(config);
5252
expect(transformed).not.toBeNull();

src/config.ts

+21-7
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import { logger } from "./logger";
44
import {
55
ConfigArrInstance,
66
ConfigCustomFormat,
7+
ConfigIncludeItem,
78
ConfigSchema,
89
InputConfigArrInstance,
910
InputConfigIncludeItem,
11+
InputConfigInstance,
1012
InputConfigSchema,
11-
YamlConfigIncludeItem,
13+
MergedConfigInstance,
1214
} from "./types/config.types";
1315
import { ROOT_PATH } from "./util";
1416

@@ -92,11 +94,7 @@ export const getSecrets = () => {
9294

9395
// 2024-09-30: Recyclarr assign_scores_to adjustments
9496
export const transformConfig = (input: InputConfigSchema): ConfigSchema => {
95-
const parseIncludes = (items: InputConfigIncludeItem[] = []): YamlConfigIncludeItem[] => {
96-
return items.map((e) => ({ template: e.template, source: e.source ?? "RECYCLARR" }));
97-
};
98-
99-
const mappedCustomFormats = (arrInput: Record<string, InputConfigArrInstance>): Record<string, ConfigArrInstance> => {
97+
const mappedCustomFormats = (arrInput: Record<string, InputConfigArrInstance> = {}): Record<string, ConfigArrInstance> => {
10098
return Object.entries(arrInput).reduce(
10199
(p, [key, value]) => {
102100
const mappedCustomFormats = (value.custom_formats || []).map<ConfigCustomFormat>((cf) => {
@@ -119,7 +117,7 @@ export const transformConfig = (input: InputConfigSchema): ConfigSchema => {
119117
return { ...rest, assign_scores_to: mapped_assign_scores };
120118
});
121119

122-
p[key] = { ...value, include: parseIncludes(value.include), custom_formats: mappedCustomFormats };
120+
p[key] = { ...value, include: value.include?.map(parseIncludes), custom_formats: mappedCustomFormats };
123121
return p;
124122
},
125123
{} as Record<string, ConfigArrInstance>,
@@ -132,3 +130,19 @@ export const transformConfig = (input: InputConfigSchema): ConfigSchema => {
132130
sonarr: mappedCustomFormats(input.sonarr),
133131
};
134132
};
133+
134+
export const parseIncludes = (input: InputConfigIncludeItem): ConfigIncludeItem => ({
135+
template: input.template,
136+
source: input.source ?? "RECYCLARR",
137+
});
138+
139+
export const validateConfig = (input: InputConfigInstance): MergedConfigInstance => {
140+
// TODO add validation and warnings like assign_scores. Setting default values not always the best
141+
return {
142+
...input,
143+
custom_formats: (input.custom_formats || []).map((e) => ({
144+
trash_ids: e.trash_ids,
145+
assign_scores_to: e.assign_scores_to ?? e.quality_profiles ?? [],
146+
})),
147+
};
148+
};

src/custom-formats.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export const manageCf = async (
3838

3939
let updatedCFs = 0;
4040
let errorCFs = 0;
41-
let validCFs = 0;
41+
const validCFss: ConfigarrCF[] = [];
4242
let createCFs = 0;
4343

4444
const manageSingle = async (carrId: string) => {
@@ -77,8 +77,7 @@ export const manageCf = async (
7777
throw new Error(`Failed updating CF ${tr.requestConfig.name}`);
7878
}
7979
} else {
80-
logger.debug(`CF ${tr.requestConfig.name} does not need update.`);
81-
validCFs++;
80+
validCFss.push(tr.carrConfig);
8281
}
8382
} else {
8483
// Create
@@ -101,7 +100,11 @@ export const manageCf = async (
101100
await manageSingle(cf);
102101
}
103102

104-
logger.info(`Created CFs: ${createCFs}, Updated CFs: ${updatedCFs}, Untouched CFs: ${validCFs}, Error CFs: ${errorCFs}`);
103+
logger.debug(
104+
validCFss.map((e) => `${e.name}`),
105+
`CFs with no update:`,
106+
);
107+
logger.info(`Created CFs: ${createCFs}, Updated CFs: ${updatedCFs}, Untouched CFs: ${validCFss.length}, Error CFs: ${errorCFs}`);
105108
};
106109
export const loadLocalCfs = async (): Promise<CFProcessing | null> => {
107110
const config = getConfig();
@@ -186,6 +189,7 @@ export const calculateCFsToManage = (yaml: ConfigCustomFormatList) => {
186189

187190
return cfTrashToManage;
188191
};
192+
189193
export const mergeCfSources = (listOfCfs: (CFProcessing | null)[]): CFProcessing => {
190194
return listOfCfs.reduce<CFProcessing>(
191195
(p, c) => {

src/quality-definitions.test.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ describe("QualityDefinitions", async () => {
127127

128128
test("calculateQualityDefinitionDiff - diff min size", async ({}) => {
129129
const clone: TrashQualityDefintion = JSON.parse(JSON.stringify(client));
130-
clone.qualities[0].min = 3;
130+
clone.qualities[0]!.min = 3;
131131

132132
const result = calculateQualityDefinitionDiff(server, clone);
133133

@@ -137,7 +137,7 @@ describe("QualityDefinitions", async () => {
137137

138138
test("calculateQualityDefinitionDiff - diff max size", async ({}) => {
139139
const clone: TrashQualityDefintion = JSON.parse(JSON.stringify(client));
140-
clone.qualities[0].max = 3;
140+
clone.qualities[0]!.max = 3;
141141

142142
const result = calculateQualityDefinitionDiff(server, clone);
143143

@@ -147,7 +147,7 @@ describe("QualityDefinitions", async () => {
147147

148148
test("calculateQualityDefinitionDiff - diff preferred size", async ({}) => {
149149
const clone: TrashQualityDefintion = JSON.parse(JSON.stringify(client));
150-
clone.qualities[0].preferred = 3;
150+
clone.qualities[0]!.preferred = 3;
151151

152152
const result = calculateQualityDefinitionDiff(server, clone);
153153

@@ -157,7 +157,7 @@ describe("QualityDefinitions", async () => {
157157

158158
test("calculateQualityDefinitionDiff - create new element", async ({}) => {
159159
const clone: TrashQualityDefintion = JSON.parse(JSON.stringify(client));
160-
clone.qualities[0].quality = "New";
160+
clone.qualities[0]!.quality = "New";
161161

162162
const result = calculateQualityDefinitionDiff(server, clone);
163163

0 commit comments

Comments
 (0)