Skip to content

Commit a5f0f92

Browse files
committed
fix: allow loading custom format definition correctly from templates
- also fixes a bug with possible CF overwrites because we loaded all CF definitions even if not needed
1 parent e91d7bd commit a5f0f92

File tree

5 files changed

+80
-65
lines changed

5 files changed

+80
-65
lines changed

src/custom-formats.test.ts

+11-15
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ describe("CustomFormats", () => {
8282
cfNameToCarrConfig: new Map([["CF2", { configarr_id: "id2", name: "CF2" }]]),
8383
};
8484

85-
const result = mergeCfSources([source1, source2, null]);
85+
const result = mergeCfSources(new Set(["id1", "id2"]), [source1, source2, null]);
8686

8787
expect(result.carrIdMapping.size).toBe(2);
8888
expect(result.cfNameToCarrConfig.size).toBe(2);
@@ -113,54 +113,50 @@ describe("CustomFormats", () => {
113113
describe("loadCustomFormatDefinitions", () => {
114114
it("should load and merge (trash CFDs", async () => {
115115
const mockTrashCFs: CFProcessing = {
116-
carrIdMapping: new Map([["trash1", { carrConfig: { configarr_id: "trash1" }, requestConfig: {} }]]),
116+
carrIdMapping: new Map([["trash1", { carrConfig: { configarr_id: "trash1", name: "trash1" }, requestConfig: {} }]]),
117117
cfNameToCarrConfig: new Map(),
118118
};
119119

120120
vi.mock("./trash-guide");
121121
vi.mocked(loadTrashCFs).mockResolvedValue(mockTrashCFs);
122122
vi.spyOn(config, "getConfig").mockReturnValue({ localCustomFormatsPath: undefined });
123123

124-
const result = await loadCustomFormatDefinitions("RADARR", []);
124+
const result = await loadCustomFormatDefinitions(new Set(["trash1"]), "RADARR", []);
125125

126126
expect(result.carrIdMapping.size).toBe(1);
127127
expect(result.carrIdMapping.has("trash1")).toBeTruthy();
128128
});
129129

130130
it("should load and merge (additional CFDs)", async () => {
131131
const mockTrashCFs: CFProcessing = {
132-
carrIdMapping: new Map([["trash1", { carrConfig: { configarr_id: "trash1" }, requestConfig: {} }]]),
132+
carrIdMapping: new Map([["trash1", { carrConfig: { configarr_id: "trash1", name: "trash1" }, requestConfig: {} }]]),
133133
cfNameToCarrConfig: new Map(),
134134
};
135135

136136
vi.mock("./trash-guide");
137137
vi.mocked(loadTrashCFs).mockResolvedValue(mockTrashCFs);
138138
vi.spyOn(config, "getConfig").mockReturnValue({ localCustomFormatsPath: undefined });
139139

140-
const result = await loadCustomFormatDefinitions("RADARR", [customCF]);
140+
const result = await loadCustomFormatDefinitions(new Set(["trash1", customCF.trash_id]), "RADARR", [customCF]);
141141

142142
expect(result.carrIdMapping.size).toBe(2);
143143
expect(result.carrIdMapping.has("trash1")).toBeTruthy();
144144
});
145145

146-
it("should load and merge (config CFDs)", async () => {
146+
it("should ignore not managed CFs", async () => {
147147
const mockTrashCFs: CFProcessing = {
148-
carrIdMapping: new Map(),
148+
carrIdMapping: new Map([["trash1", { carrConfig: { configarr_id: "trash1", name: "trash1" }, requestConfig: {} }]]),
149149
cfNameToCarrConfig: new Map(),
150150
};
151151

152-
const clonedCFD: TrashCF = JSON.parse(JSON.stringify(customCF));
153-
clonedCFD.trash_id = "trash2";
154-
clonedCFD.name = "Trash2";
155-
156152
vi.mock("./trash-guide");
157153
vi.mocked(loadTrashCFs).mockResolvedValue(mockTrashCFs);
158-
vi.spyOn(config, "getConfig").mockReturnValue({ localCustomFormatsPath: undefined, customFormatDefinitions: [customCF] });
154+
vi.spyOn(config, "getConfig").mockReturnValue({ localCustomFormatsPath: undefined });
159155

160-
const result = await loadCustomFormatDefinitions("RADARR", [clonedCFD]);
156+
const result = await loadCustomFormatDefinitions(new Set(["trash1"]), "RADARR", [customCF]);
161157

162-
expect(result.carrIdMapping.size).toBe(2);
163-
expect(result.carrIdMapping.has("trash2")).toBeTruthy();
158+
expect(result.carrIdMapping.size).toBe(1);
159+
expect(result.carrIdMapping.has("trash1")).toBeTruthy();
164160
});
165161
});
166162
});

src/custom-formats.ts

+19-15
Original file line numberDiff line numberDiff line change
@@ -195,16 +195,16 @@ export const mapCustomFormatDefinitions = (customFormatDefinitions: CustomFormat
195195
};
196196
};
197197

198-
export const loadCustomFormatDefinitions = async (arrType: ArrType, additionalCFDs: CustomFormatDefinitions) => {
198+
export const loadCustomFormatDefinitions = async (idsToMange: Set<string>, arrType: ArrType, additionalCFDs: CustomFormatDefinitions) => {
199+
// TODO: the object CFProcessing is only needed as result from this method. All other should only work with ID -> object
199200
const trashCFs = await loadTrashCFs(arrType);
200201
const localFileCFs = await loadLocalCfs();
201-
const configCFDs = loadCFFromConfig();
202202

203203
logger.debug(
204-
`Loaded ${trashCFs.carrIdMapping.size} TrashCFs, ${localFileCFs?.carrIdMapping.size} LocalCFs, ${configCFDs?.carrIdMapping.size} ConfigCFs, ${additionalCFDs.length} AdditionalCFs`,
204+
`Total loaded CF definitions: ${trashCFs.carrIdMapping.size} TrashCFs, ${localFileCFs?.carrIdMapping.size == null ? 0 : localFileCFs?.carrIdMapping.size} LocalCFs, ${additionalCFDs.length} ConfigCFs`,
205205
);
206206

207-
return mergeCfSources([trashCFs, localFileCFs, configCFDs, mapCustomFormatDefinitions(additionalCFDs)]);
207+
return mergeCfSources(idsToMange, [trashCFs, localFileCFs, mapCustomFormatDefinitions(additionalCFDs)]);
208208
};
209209

210210
export const calculateCFsToManage = (yaml: ConfigCustomFormatList) => {
@@ -219,25 +219,29 @@ export const calculateCFsToManage = (yaml: ConfigCustomFormatList) => {
219219
return cfTrashToManage;
220220
};
221221

222-
export const mergeCfSources = (listOfCfs: (CFProcessing | null)[]): CFProcessing => {
222+
export const mergeCfSources = (idsToManage: Set<string>, listOfCfs: (CFProcessing | null)[]): CFProcessing => {
223223
return listOfCfs.reduce<CFProcessing>(
224224
(p, c) => {
225225
if (!c) {
226226
return p;
227227
}
228228

229-
for (const [key, value] of c.carrIdMapping.entries()) {
230-
if (p.carrIdMapping.has(key)) {
231-
logger.info(`Overwriting ${key} during CF merge`);
232-
}
233-
p.carrIdMapping.set(key, value);
234-
}
229+
for (const test of idsToManage) {
230+
const value = c.carrIdMapping.get(test);
231+
const cfName = value?.carrConfig.name!;
232+
233+
if (value) {
234+
if (p.carrIdMapping.has(test)) {
235+
logger.warn(`Overwriting CF with id '${test}' during merge.`);
236+
}
237+
238+
if (p.cfNameToCarrConfig.has(cfName)) {
239+
logger.warn(`Overwriting CF with name '${cfName}' (ID: ${test}) during merge.`);
240+
}
235241

236-
for (const [key, value] of c.cfNameToCarrConfig.entries()) {
237-
if (p.cfNameToCarrConfig.has(key)) {
238-
logger.info(`Overwriting ${key} during CF merge`);
242+
p.carrIdMapping.set(test, value);
243+
p.cfNameToCarrConfig.set(value.carrConfig.name!, value.carrConfig);
239244
}
240-
p.cfNameToCarrConfig.set(key, value);
241245
}
242246

243247
return p;

src/index.ts

+46-31
Original file line numberDiff line numberDiff line change
@@ -21,26 +21,18 @@ import {
2121
transformTrashQPCFs,
2222
transformTrashQPToTemplate,
2323
} from "./trash-guide";
24-
import { ArrType, CFProcessing, MappedMergedTemplates } from "./types/common.types";
25-
import {
26-
ConfigQualityProfile,
27-
CustomFormatDefinitions,
28-
InputConfigArrInstance,
29-
InputConfigIncludeItem,
30-
MergedConfigInstance,
31-
} from "./types/config.types";
24+
import { ArrType, MappedMergedTemplates } from "./types/common.types";
25+
import { ConfigQualityProfile, InputConfigArrInstance, InputConfigIncludeItem, MergedConfigInstance } from "./types/config.types";
3226
import { TrashQualityDefintion } from "./types/trashguide.types";
3327

3428
/**
3529
* Load data from trash, recyclarr, custom configs and merge.
3630
* Afterwards do sanitize and check against required configuration.
31+
* TODO: probably move to config.ts and write tests for it for different merge scenarios
3732
* @param value
3833
* @param arrType
3934
*/
40-
const mergeConfigsAndTemplates = async (
41-
value: InputConfigArrInstance,
42-
arrType: ArrType,
43-
): Promise<{ mergedCFs: CFProcessing; config: MergedConfigInstance }> => {
35+
const mergeConfigsAndTemplates = async (value: InputConfigArrInstance, arrType: ArrType): Promise<{ config: MergedConfigInstance }> => {
4436
const recyclarrTemplateMap = loadRecyclarrTemplates(arrType);
4537
const localTemplateMap = loadLocalRecyclarrTemplate(arrType);
4638
const trashTemplates = await loadQPFromTrash(arrType);
@@ -54,7 +46,7 @@ const mergeConfigsAndTemplates = async (
5446
quality_profiles: [],
5547
};
5648

57-
// TODO: customFormatDefinitions not supported in templates yet
49+
// HINT: we assume customFormatDefinitions only exist in RECYCLARR
5850
if (value.include) {
5951
const mappedIncludes = value.include.reduce<{ recyclarr: InputConfigIncludeItem[]; trash: InputConfigIncludeItem[] }>(
6052
(previous, current) => {
@@ -108,6 +100,17 @@ const mergeConfigsAndTemplates = async (
108100
recyclarrMergedTemplates.media_naming = { ...recyclarrMergedTemplates.media_naming, ...template.media_naming };
109101
}
110102

103+
if (template.customFormatDefinitions) {
104+
if (Array.isArray(template.customFormatDefinitions)) {
105+
recyclarrMergedTemplates.customFormatDefinitions = [
106+
...(recyclarrMergedTemplates.customFormatDefinitions || []),
107+
...template.customFormatDefinitions,
108+
];
109+
} else {
110+
logger.warn(`CustomFormatDefinitions in template must be an array. Ignoring.`);
111+
}
112+
}
113+
111114
// TODO Ignore recursive include for now
112115
if (template.include) {
113116
logger.warn(`Recursive includes not supported at the moment. Ignoring.`);
@@ -128,6 +131,7 @@ const mergeConfigsAndTemplates = async (
128131
});
129132
}
130133

134+
// Config values overwrite template values
131135
if (value.custom_formats) {
132136
recyclarrMergedTemplates.custom_formats.push(...value.custom_formats);
133137
}
@@ -148,6 +152,17 @@ const mergeConfigsAndTemplates = async (
148152
recyclarrMergedTemplates.quality_definition = { ...recyclarrMergedTemplates.quality_definition, ...value.quality_definition };
149153
}
150154

155+
if (value.customFormatDefinitions) {
156+
if (Array.isArray(value.customFormatDefinitions)) {
157+
recyclarrMergedTemplates.customFormatDefinitions = [
158+
...(recyclarrMergedTemplates.customFormatDefinitions || []),
159+
...value.customFormatDefinitions,
160+
];
161+
} else {
162+
logger.warn(`CustomFormatDefinitions in config file must be an array. Ignoring.`);
163+
}
164+
}
165+
151166
const recyclarrProfilesMerged = recyclarrMergedTemplates.quality_profiles.reduce<Map<string, ConfigQualityProfile>>((p, c) => {
152167
const profile = p.get(c.name);
153168

@@ -175,21 +190,6 @@ const mergeConfigsAndTemplates = async (
175190

176191
recyclarrMergedTemplates.quality_profiles = filterInvalidQualityProfiles(recyclarrMergedTemplates.quality_profiles);
177192

178-
/*
179-
TODO: do we want to load all available local templates or only the included ones in the instance?
180-
Example: we have a local template folder which we can always traverse. So we could load every CF defined there.
181-
But then we could also have in theory conflicted CF IDs if user want to define same CF in different templates.
182-
How to handle overwrite? Maybe also support overriding CFs defined in Trash or something?
183-
*/
184-
const localTemplateCFDs = Array.from(localTemplateMap.values()).reduce((p, c) => {
185-
if (c.customFormatDefinitions) {
186-
p.push(...c.customFormatDefinitions);
187-
}
188-
return p;
189-
}, [] as CustomFormatDefinitions);
190-
191-
const mergedCFs = await loadCustomFormatDefinitions(arrType, localTemplateCFDs);
192-
193193
// merge profiles from recyclarr templates into one
194194
const qualityProfilesMerged = recyclarrMergedTemplates.quality_profiles.reduce((p, c) => {
195195
let existingQp = p.get(c.name);
@@ -218,18 +218,33 @@ const mergeConfigsAndTemplates = async (
218218

219219
const validatedConfig = validateConfig(recyclarrMergedTemplates);
220220
logger.debug(`Merged config: '${JSON.stringify(validatedConfig)}'`);
221-
return { mergedCFs: mergedCFs, config: validatedConfig };
221+
222+
/*
223+
TODO: do we want to load all available local templates or only the included ones in the instance?
224+
Example: we have a local template folder which we can always traverse. So we could load every CF defined there.
225+
But then we could also have in theory conflicted CF IDs if user want to define same CF in different templates.
226+
How to handle overwrite? Maybe also support overriding CFs defined in Trash or something?
227+
*/
228+
// const localTemplateCFDs = Array.from(localTemplateMap.values()).reduce((p, c) => {
229+
// if (c.customFormatDefinitions) {
230+
// p.push(...c.customFormatDefinitions);
231+
// }
232+
// return p;
233+
// }, [] as CustomFormatDefinitions);
234+
235+
return { config: validatedConfig };
222236
};
223237

224238
const pipeline = async (value: InputConfigArrInstance, arrType: ArrType) => {
225239
const api = getUnifiedClient();
226240

227-
const { config, mergedCFs } = await mergeConfigsAndTemplates(value, arrType);
241+
const { config } = await mergeConfigsAndTemplates(value, arrType);
228242

229243
const idsToManage = calculateCFsToManage(config);
230-
231244
logger.debug(Array.from(idsToManage), `CustomFormats to manage`);
232245

246+
const mergedCFs = await loadCustomFormatDefinitions(idsToManage, arrType, config.customFormatDefinitions || []);
247+
233248
let serverCFs = await loadServerCustomFormats();
234249
logger.info(`CustomFormats on server: ${serverCFs.length}`);
235250

src/types/common.types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export type TCM = TC1 | TC2;
4242

4343
export type ImportCF = OmitTyped<MergedCustomFormatResource, "specifications"> & {
4444
specifications?: TCM[] | null;
45-
};
45+
} & Required<Pick<MergedCustomFormatResource, "name">>;
4646

4747
export type ConfigarrCFMeta = {
4848
configarr_id: string;

src/types/config.types.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { ConfigarrCFMeta } from "./common.types";
2-
import { TrashCFMeta, TrashScores } from "./trashguide.types";
1+
import { ConfigarrCF } from "./common.types";
2+
import { TrashCF, TrashScores } from "./trashguide.types";
33

4-
export type CustomFormatDefinitions = (TrashCFMeta | ConfigarrCFMeta)[];
4+
export type CustomFormatDefinitions = (TrashCF | ConfigarrCF)[];
55

66
export type InputConfigSchema = {
77
trashGuideUrl?: string;

0 commit comments

Comments
 (0)