|
1 | 1 | import { existsSync, readFileSync } from "node:fs";
|
2 | 2 | import yaml from "yaml";
|
3 | 3 | import { getHelpers } from "./env";
|
| 4 | +import { loadLocalRecyclarrTemplate } from "./local-importer"; |
4 | 5 | import { logger } from "./logger";
|
| 6 | +import { filterInvalidQualityProfiles } from "./quality-profiles"; |
| 7 | +import { loadRecyclarrTemplates } from "./recyclarr-importer"; |
| 8 | +import { loadQPFromTrash, transformTrashQPCFs, transformTrashQPToTemplate } from "./trash-guide"; |
| 9 | +import { ArrType, MappedMergedTemplates } from "./types/common.types"; |
5 | 10 | import {
|
6 | 11 | ConfigArrInstance,
|
7 | 12 | ConfigCustomFormat,
|
8 | 13 | ConfigIncludeItem,
|
| 14 | + ConfigQualityProfile, |
9 | 15 | ConfigSchema,
|
10 | 16 | InputConfigArrInstance,
|
11 | 17 | InputConfigIncludeItem,
|
@@ -151,3 +157,215 @@ export const validateConfig = (input: InputConfigInstance): MergedConfigInstance
|
151 | 157 | })),
|
152 | 158 | };
|
153 | 159 | };
|
| 160 | + |
| 161 | +/** |
| 162 | + * Load data from trash, recyclarr, custom configs and merge. |
| 163 | + * Afterwards do sanitize and check against required configuration. |
| 164 | + * @param value |
| 165 | + * @param arrType |
| 166 | + */ |
| 167 | +export const mergeConfigsAndTemplates = async ( |
| 168 | + value: InputConfigArrInstance, |
| 169 | + arrType: ArrType, |
| 170 | +): Promise<{ config: MergedConfigInstance }> => { |
| 171 | + const recyclarrTemplateMap = loadRecyclarrTemplates(arrType); |
| 172 | + const localTemplateMap = loadLocalRecyclarrTemplate(arrType); |
| 173 | + const trashTemplates = await loadQPFromTrash(arrType); |
| 174 | + |
| 175 | + logger.debug( |
| 176 | + `Loaded ${recyclarrTemplateMap.size} Recyclarr templates, ${localTemplateMap.size} local templates and ${trashTemplates.size} trash templates.`, |
| 177 | + ); |
| 178 | + |
| 179 | + const recyclarrMergedTemplates: MappedMergedTemplates = { |
| 180 | + custom_formats: [], |
| 181 | + quality_profiles: [], |
| 182 | + }; |
| 183 | + |
| 184 | + // HINT: we assume customFormatDefinitions only exist in RECYCLARR |
| 185 | + if (value.include) { |
| 186 | + const mappedIncludes = value.include.reduce<{ recyclarr: InputConfigIncludeItem[]; trash: InputConfigIncludeItem[] }>( |
| 187 | + (previous, current) => { |
| 188 | + switch (current.source) { |
| 189 | + case "TRASH": |
| 190 | + previous.trash.push(current); |
| 191 | + break; |
| 192 | + case "RECYCLARR": |
| 193 | + previous.recyclarr.push(current); |
| 194 | + break; |
| 195 | + default: |
| 196 | + logger.warn(`Unknown type for template requested: ${(current as any).type}. Ignoring.`); |
| 197 | + } |
| 198 | + |
| 199 | + return previous; |
| 200 | + }, |
| 201 | + { recyclarr: [], trash: [] }, |
| 202 | + ); |
| 203 | + |
| 204 | + logger.info( |
| 205 | + `Found ${value.include.length} templates to include. Mapped to [recyclarr]=${mappedIncludes.recyclarr.length}, [trash]=${mappedIncludes.trash.length} ...`, |
| 206 | + ); |
| 207 | + |
| 208 | + mappedIncludes.recyclarr.forEach((e) => { |
| 209 | + const template = recyclarrTemplateMap.get(e.template) ?? localTemplateMap.get(e.template); |
| 210 | + |
| 211 | + if (!template) { |
| 212 | + logger.warn(`Unknown recyclarr template requested: ${e.template}`); |
| 213 | + return; |
| 214 | + } |
| 215 | + |
| 216 | + if (template.custom_formats) { |
| 217 | + recyclarrMergedTemplates.custom_formats?.push(...template.custom_formats); |
| 218 | + } |
| 219 | + |
| 220 | + if (template.quality_definition) { |
| 221 | + recyclarrMergedTemplates.quality_definition = template.quality_definition; |
| 222 | + } |
| 223 | + |
| 224 | + if (template.quality_profiles) { |
| 225 | + for (const qp of template.quality_profiles) { |
| 226 | + recyclarrMergedTemplates.quality_profiles.push(qp); |
| 227 | + } |
| 228 | + } |
| 229 | + |
| 230 | + if (template.media_management) { |
| 231 | + recyclarrMergedTemplates.media_management = { ...recyclarrMergedTemplates.media_management, ...template.media_management }; |
| 232 | + } |
| 233 | + |
| 234 | + if (template.media_naming) { |
| 235 | + recyclarrMergedTemplates.media_naming = { ...recyclarrMergedTemplates.media_naming, ...template.media_naming }; |
| 236 | + } |
| 237 | + |
| 238 | + if (template.customFormatDefinitions) { |
| 239 | + if (Array.isArray(template.customFormatDefinitions)) { |
| 240 | + recyclarrMergedTemplates.customFormatDefinitions = [ |
| 241 | + ...(recyclarrMergedTemplates.customFormatDefinitions || []), |
| 242 | + ...template.customFormatDefinitions, |
| 243 | + ]; |
| 244 | + } else { |
| 245 | + logger.warn(`CustomFormatDefinitions in template must be an array. Ignoring.`); |
| 246 | + } |
| 247 | + } |
| 248 | + |
| 249 | + // TODO Ignore recursive include for now |
| 250 | + if (template.include) { |
| 251 | + logger.warn(`Recursive includes not supported at the moment. Ignoring.`); |
| 252 | + } |
| 253 | + }); |
| 254 | + |
| 255 | + // TODO: local TRaSH-Guides QP templates do not work yet |
| 256 | + mappedIncludes.trash.forEach((e) => { |
| 257 | + const template = trashTemplates.get(e.template); |
| 258 | + |
| 259 | + if (!template) { |
| 260 | + logger.warn(`Unknown trash template requested: ${e.template}`); |
| 261 | + return; |
| 262 | + } |
| 263 | + |
| 264 | + recyclarrMergedTemplates.quality_profiles.push(transformTrashQPToTemplate(template)); |
| 265 | + recyclarrMergedTemplates.custom_formats.push(transformTrashQPCFs(template)); |
| 266 | + }); |
| 267 | + } |
| 268 | + |
| 269 | + // Config values overwrite template values |
| 270 | + if (value.custom_formats) { |
| 271 | + recyclarrMergedTemplates.custom_formats.push(...value.custom_formats); |
| 272 | + } |
| 273 | + |
| 274 | + if (value.quality_profiles) { |
| 275 | + recyclarrMergedTemplates.quality_profiles.push(...value.quality_profiles); |
| 276 | + } |
| 277 | + |
| 278 | + if (value.media_management) { |
| 279 | + recyclarrMergedTemplates.media_management = { ...recyclarrMergedTemplates.media_management, ...value.media_management }; |
| 280 | + } |
| 281 | + |
| 282 | + if (value.media_naming) { |
| 283 | + recyclarrMergedTemplates.media_naming = { ...recyclarrMergedTemplates.media_naming, ...value.media_naming }; |
| 284 | + } |
| 285 | + |
| 286 | + if (value.quality_definition) { |
| 287 | + recyclarrMergedTemplates.quality_definition = { ...recyclarrMergedTemplates.quality_definition, ...value.quality_definition }; |
| 288 | + } |
| 289 | + |
| 290 | + if (value.customFormatDefinitions) { |
| 291 | + if (Array.isArray(value.customFormatDefinitions)) { |
| 292 | + recyclarrMergedTemplates.customFormatDefinitions = [ |
| 293 | + ...(recyclarrMergedTemplates.customFormatDefinitions || []), |
| 294 | + ...value.customFormatDefinitions, |
| 295 | + ]; |
| 296 | + } else { |
| 297 | + logger.warn(`CustomFormatDefinitions in config file must be an array. Ignoring.`); |
| 298 | + } |
| 299 | + } |
| 300 | + |
| 301 | + const recyclarrProfilesMerged = recyclarrMergedTemplates.quality_profiles.reduce<Map<string, ConfigQualityProfile>>((p, c) => { |
| 302 | + const profile = p.get(c.name); |
| 303 | + |
| 304 | + if (profile == null) { |
| 305 | + p.set(c.name, c); |
| 306 | + } else { |
| 307 | + p.set(c.name, { |
| 308 | + ...profile, |
| 309 | + ...c, |
| 310 | + reset_unmatched_scores: { |
| 311 | + enabled: c.reset_unmatched_scores?.enabled ?? profile.reset_unmatched_scores?.enabled ?? true, |
| 312 | + except: c.reset_unmatched_scores?.except ?? profile.reset_unmatched_scores?.except, |
| 313 | + }, |
| 314 | + upgrade: { |
| 315 | + ...profile.upgrade, |
| 316 | + ...c.upgrade, |
| 317 | + }, |
| 318 | + }); |
| 319 | + } |
| 320 | + |
| 321 | + return p; |
| 322 | + }, new Map()); |
| 323 | + |
| 324 | + recyclarrMergedTemplates.quality_profiles = Array.from(recyclarrProfilesMerged.values()); |
| 325 | + |
| 326 | + recyclarrMergedTemplates.quality_profiles = filterInvalidQualityProfiles(recyclarrMergedTemplates.quality_profiles); |
| 327 | + |
| 328 | + // merge profiles from recyclarr templates into one |
| 329 | + const qualityProfilesMerged = recyclarrMergedTemplates.quality_profiles.reduce((p, c) => { |
| 330 | + let existingQp = p.get(c.name); |
| 331 | + |
| 332 | + if (!existingQp) { |
| 333 | + p.set(c.name, { ...c }); |
| 334 | + } else { |
| 335 | + existingQp = { |
| 336 | + ...existingQp, |
| 337 | + ...c, |
| 338 | + // Overwriting qualities array for now |
| 339 | + upgrade: { ...existingQp.upgrade, ...c.upgrade }, |
| 340 | + reset_unmatched_scores: { |
| 341 | + ...existingQp.reset_unmatched_scores, |
| 342 | + ...c.reset_unmatched_scores, |
| 343 | + enabled: (c.reset_unmatched_scores?.enabled ?? existingQp.reset_unmatched_scores?.enabled) || false, |
| 344 | + }, |
| 345 | + }; |
| 346 | + p.set(c.name, existingQp); |
| 347 | + } |
| 348 | + |
| 349 | + return p; |
| 350 | + }, new Map<string, ConfigQualityProfile>()); |
| 351 | + |
| 352 | + recyclarrMergedTemplates.quality_profiles = Array.from(qualityProfilesMerged.values()); |
| 353 | + |
| 354 | + const validatedConfig = validateConfig(recyclarrMergedTemplates); |
| 355 | + logger.debug(`Merged config: '${JSON.stringify(validatedConfig)}'`); |
| 356 | + |
| 357 | + /* |
| 358 | + TODO: do we want to load all available local templates or only the included ones in the instance? |
| 359 | + Example: we have a local template folder which we can always traverse. So we could load every CF defined there. |
| 360 | + But then we could also have in theory conflicted CF IDs if user want to define same CF in different templates. |
| 361 | + How to handle overwrite? Maybe also support overriding CFs defined in Trash or something? |
| 362 | + */ |
| 363 | + // const localTemplateCFDs = Array.from(localTemplateMap.values()).reduce((p, c) => { |
| 364 | + // if (c.customFormatDefinitions) { |
| 365 | + // p.push(...c.customFormatDefinitions); |
| 366 | + // } |
| 367 | + // return p; |
| 368 | + // }, [] as CustomFormatDefinitions); |
| 369 | + |
| 370 | + return { config: validatedConfig }; |
| 371 | +}; |
0 commit comments