Conversation
Closes raydak-labs#518. Adds two related ways to keep a quality profile in its own file and share it: 1. `include:` now accepts a filesystem path, alongside template names and URLs: include: - template: ./profiles/radarr/uhd.yml Relative paths resolve against the directory holding config.yml (not the working directory). Recyclarr YAML and TRaSH JSON are both accepted, with TRaSH detected automatically from `trash_id`. A value that matches a known Recyclarr/local/TRaSH template key still resolves as that template, so no existing config changes meaning. 2. A new instance-level `profiles:` block binds a name-free template to a name: profiles: - name: UHD includes: ./profiles/radarr/quality.yml The included file names nothing; the profile name comes from the config, so the same file can be reused across instances under different names. Each entry is resolved in isolation and accepts every include form, then its quality profile and all custom format score assignments are bound to `name`. An entry resolving to more than one quality profile is skipped with a warning, since there would be no single name to bind. Previously this was only possible via `localConfigTemplatesPath`, a single flat directory keyed by basename, and the profile name still had to be hardcoded in the file - which `renameQualityProfiles`/`cloneQualityProfiles` could only patch up after the fact. Also hardens `mergeAndReduceCustomFormats` against a score assignment with no profile name, which template files could already produce (they bypass schema validation) and which previously created a quality profile named "undefined". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVQLWA3hsfKjrX4YsaPk6C
Reviewer's GuideAdds config-relative file-path includes for Recyclarr and TRaSH templates, plus reusable Sequence diagram for config-relative template and reusable profile resolutionsequenceDiagram
participant Config as Config merger
participant Maps as Template maps
participant File as File template importer
participant Scratch as Profile scratch buffer
participant Merge as Merged config
Config->>Maps: Resolve include template keys
alt Known template key
Maps-->>Config: Return mapped template
else File path
Config->>File: loadTemplateFromFile(template)
File-->>Config: Return Recyclarr or TRASH template
end
Config->>Merge: Apply include template
opt profiles entry
Config->>Scratch: Resolve profile.includes in isolation
Scratch->>Maps: Resolve all include forms
Maps-->>Scratch: Populate scratch templates
Config->>Scratch: bindProfileName(name)
Scratch-->>Config: Rebound profile and score assignments
Config->>Merge: Merge bound profile
end
Config->>Merge: Apply instance configuration
Flow diagram for reusable profile bindingflowchart TD
A[profiles entry: name + includes] --> B[Resolve includes in scratch buffer]
B --> C{Quality profiles resolved}
C -->|More than one| D[Warn and skip entry]
C -->|Zero| E[Bind custom format scores only]
C -->|Exactly one| F[Rename profile to configured name]
E --> G[Rebind or create score assignments]
F --> G
G --> H[Merge into instance config]
H --> I[Apply instance config and later rename or clone operations]
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 1 issue
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="src/config.ts" line_range="736-739" />
<code_context>
+ // managed elsewhere (or already present on the server).
+ logger.debug(`Profile '${name}': includes resolve to no quality profile. Binding custom format scores only.`);
+ } else {
+ if (qualityProfile.name !== name) {
+ logger.info(`Profile '${name}': bound included quality profile '${qualityProfile.name}' -> '${name}'.`);
+ }
+ qualityProfile.name = name;
+ }
+
</code_context>
<issue_to_address>
**issue (broader_impact):** Binding a profile that comes from a Recyclarr or local template mutates the objects stored in the shared template map. When two `profiles` entries reuse the same named template, binding the second entry overwrites the quality-profile name and score-assignment names already appended for the first entry, so both entries end up under the second name.
**Triggers:** When multiple `profiles` entries reuse the same Recyclarr or local template name under different configured names.
**Suggested fix:** Deep-clone the resolved template before placing it in the scratch buffer or before rebinding it.
</issue_to_address>Sourcery assessment
Needs a human reviewer. 1 finding to address first, and a path or profile-binding bug could apply the wrong quality profile or custom-format scores to the configured Radarr/Sonarr instance. Reverting prevents future application, but settings already written to those instances would remain and require a rerun or manual repair.
Blocking findings: src/config.ts:739
| if (qualityProfile.name !== name) { | ||
| logger.info(`Profile '${name}': bound included quality profile '${qualityProfile.name}' -> '${name}'.`); | ||
| } | ||
| qualityProfile.name = name; |
There was a problem hiding this comment.
issue (broader_impact): Binding a profile that comes from a Recyclarr or local template mutates the objects stored in the shared template map. When two profiles entries reuse the same named template, binding the second entry overwrites the quality-profile name and score-assignment names already appended for the first entry, so both entries end up under the second name.
Triggers: When multiple profiles entries reuse the same Recyclarr or local template name under different configured names.
Suggested fix: Deep-clone the resolved template before placing it in the scratch buffer or before rebinding it.
Closes #518.
Problem
The issue asks to keep a quality profile in its own file and share it across instances, independent of
base_url/api_keyand of the profile name.Today that isn't quite possible:
include:accepts a Recyclarr/local template name, a TRaSHtrash_id, or anhttp(s)URL — never a filesystem path.localConfigTemplatesPathdirectory keyed by basename, soprofiles/radarr/uhd.ymlis unreachable, names are global across arr types, and collisions silently overwrite.config.yml, so they can't share anything with a separate file.renameQualityProfiles/cloneQualityProfilesonly patch it up afterwards — which is exactly the coupling the issue wants removed.Changes
1. Include a template by file path
New
src/file-template-importer.ts, modelled directly on the existingurl-template-importer.ts(same shape, same "log and return null" contract, synchronous).config.yml, never the working directory.trash_idis detected as TRaSH, sosource: TRASHis optional (an improvement over URL includes, which still require it).2.
profiles:— bind a name-free template to a nameEach entry is resolved in isolation through the existing include pipeline into a scratch buffer, then its names are bound and it is merged normally. Because it reuses that pipeline,
includesaccepts every include form — file, URL, Recyclarr name, TRaSH id — not just files.Binding rules:
name, whatever it was calledassign_scores_tonameis createdassign_scores_toentryname; scores preservedScore assignments are rebound even when they already name a profile — that is what lets an unmodified upstream template be reused under a name of your choosing. It's unambiguous because an entry resolving to more than one quality profile is rejected, so there is never a second legitimate target.
Ordering: after
include:, before the instance's owncustom_formats/quality_profiles(so instance config still wins), and beforerenameQualityProfiles/cloneQualityProfiles(so a bound profile can still be renamed or cloned).3. Drive-by fix
mergeAndReduceCustomFormatsnow drops a score assignment with no profile name instead of creating a quality profile literally namedundefined. Template files bypass schema validation entirely (yaml.parse(...) as MappedTemplates), so this was already reachable before this PR.Notes for review
includes:— it matches the issue text, but every other key in the codebase is singularinclude:. Happy to rename.include:inside a profile file still warns and is ignored, as before. Proper recursion needs cycle detection and a decision on what relative paths inside an included file resolve against — worth its own issue.file_templatesandconfig_profilesflags.Testing
pnpm build && pnpm test && pnpm lint && pnpm typecheckall pass — 449 tests, 35 new.src/file-template-importer.test.ts(new, 17 tests): path detection, config-relative resolution, and every load failure mode.src/config.test.ts: file includes (relative, absolute, TRaSH JSON, missing file, template-key-wins regression) andprofiles:(nameless binding, synthesised assignments, rebinding, >1 profile skip, one file under two names, Recyclarr template in a profile, interaction with rename, noprofilesleak into the merged config).Also verified outside the test suite, running from a different working directory: one nameless file bound to two names produced two independent profiles with correct scores, confirming config-dir path resolution.
Docs updated in
config-file.md(new "File Templates" and "Reusable Profiles" sections),general.md(merge order), the config sample, and a working example underexamples/full/config/profiles/.🤖 Generated with Claude Code
https://claude.ai/code/session_01HVQLWA3hsfKjrX4YsaPk6C
Summary by Sourcery
Support reusable, name-independent profiles and direct template-file includes while preserving existing template resolution behavior.
New Features:
Bug Fixes:
undefinedduring template merging.Enhancements:
Documentation:
Tests: