Skip to content

feat: Tag formatting options#2504

Open
MagicLegend wants to merge 9 commits intoseerr-team:developfrom
MagicLegend:tag-formatting-options
Open

feat: Tag formatting options#2504
MagicLegend wants to merge 9 commits intoseerr-team:developfrom
MagicLegend:tag-formatting-options

Conversation

@MagicLegend
Copy link

@MagicLegend MagicLegend commented Feb 18, 2026

Description

Took the liberty of implementing the proposed dropdown, indentation and hiding changes proposed by fallenbagel in #2282 (comment).

This creates an option for the user to choose how they want the tags formatted.

Note that in my Radarr instance migration 0007 did not actually update the tags to the new format. Wasn't it supposed to do this automatically? (See screenshot below) I'm aware that the current matching logic could have collisions if the user changes to a different format and has an username starting with a number followed by a dash that also matches an existing label. (e.g. 1-the-man).

AI was used for this PR. Please open me for more details.

Model used: Anthropic Claude Sonnet 4.6.

Prompt:

Analyse the nested structure of the Permissions. Reimplement the "tagRequestsNoID" checkbox to be a child of "tagRequests", and to be a dropdown with the options "userid", "userid - username" and "username" to indicate the different ways requests can be tagged. Then, look at MediaRequestSubscriber and adjust the tagging logic to support the three options.

Context files given:
src/components/Settings/RadarrModal/index.tsx
src/components/PermissionOption/index.tsx
server/subscriber/MediaRequestSubscriber.ts

Note that it took the changes to the French i18n file and updated those, and created a migration to accommodate the changed config value. Given that the change this was built upon has not reached GA yet, I removed the migration. I also did not include the changes it made to the French i18n file because I could not validate the translation myself.

Complete reasoning output of the initial changes:

Read [](file:///home/magiclegend/Documents/private/seerr/server/subscriber/MediaRequestSubscriber.ts#290-290), lines 290 to 400

Read [](file:///home/magiclegend/Documents/private/seerr/src/components/Settings/RadarrModal/index.tsx#1-1), lines 1 to 100

Read [](file:///home/magiclegend/Documents/private/seerr/src/components/Settings/RadarrModal/index.tsx#650-650), lines 650 to 759

Read [](file:///home/magiclegend/Documents/private/seerr/server/subscriber/MediaRequestSubscriber.ts#668-668), lines 668 to 750

Now let me look at the settings types and the SonarrModal to understand the full picture. 

Searched for text `tagRequestsNoID|tagRequests` (`**/server/lib/settings.ts`), no results

Searched for files matching `**/src/components/Settings/SonarrModal/index.tsx`, 1 match

Searched for text `tagRequestsNoID|tagRequests` (`**/server/lib/settings.ts`), no results

Searched for text `tagRequestsNoID|tagRequests` (`**/src/components/Settings/SonarrModal/index.tsx`), 20 results

Searched for files matching `**/server/lib/settings.ts`, no matches

Read [](file:///home/magiclegend/Documents/private/seerr/src/components/Settings/SonarrModal/index.tsx#1010-1010), lines 1010 to 1056

Searched for text `tagRequestsNoID|tagRequests|RadarrSettings|SonarrSettings` (`**/server/lib/**"`), no results

Searched for files matching `**/server/lib/settings*`, no matches

Read [](file:///home/magiclegend/Documents/private/seerr/server)

Read [](file:///home/magiclegend/Documents/private/seerr/server/lib)

Read [](file:///home/magiclegend/Documents/private/seerr/server/lib/settings)

Searched for text `tagRequestsNoID|tagRequests|RadarrSettings|SonarrSettings` (`**/server/lib/settings/index.ts`), 10 results

Read [](file:///home/magiclegend/Documents/private/seerr/server/lib/settings/index.ts#60-60), lines 60 to 130

Let me look at the SonarrModal for nested field patterns and the full form structure: 

Read [](file:///home/magiclegend/Documents/private/seerr/src/components/Settings/SonarrModal/index.tsx#1-1), lines 1 to 300

Let me check the routes for Radarr/Sonarr settings and any migrations: 

Searched for text `tagRequestsNoID|tagRequests` (`**/server/routes/**`), no results

Read [](file:///home/magiclegend/Documents/private/seerr/server/lib/settings/migrations)

Read [](file:///home/magiclegend/Documents/private/seerr/server/lib/settings/migrations/0007_migrate_arr_tags.ts#1-1), lines 1 to 80

Read [](file:///home/magiclegend/Documents/private/seerr/server/lib/settings/migrator.ts#1-1), lines 1 to 30

Now I have a complete picture. Let me plan the changes:

1. **index.ts** — Replace `tagRequestsNoID: boolean` with `tagRequestsFormat: 'userid' | 'userid-username' | 'username'`
2. **index.tsx** — Replace the `tagRequestsNoID` checkbox with a nested dropdown under `tagRequests`
3. **index.tsx** — Same as RadarrModal
4. **MediaRequestSubscriber.ts** — Update tagging logic for three options
5. **`server/lib/settings/migrations/0008_migrate_tag_requests_format.ts`** — Migrate existing boolean to new format string 

Created 5 todos

Starting: *Update DVRSettings type in settings* (1/5)

Using "Replace String in File"

Starting: *Add settings migration for tagRequestsFormat* (2/5)

Read [](file:///home/magiclegend/Documents/private/seerr/server/lib/settings/migrations/0007_migrate_arr_tags.ts#80-80), lines 80 to 114

Created [](file:///home/magiclegend/Documents/private/seerr/server/lib/settings/migrations/0008_migrate_tag_requests_format.ts)

Starting: *Update RadarrModal with nested dropdown* (3/5)

Now let me read the full RadarrModal form submission and initial values: 

Read [](file:///home/magiclegend/Documents/private/seerr/src/components/Settings/RadarrModal/index.tsx#200-200), lines 200 to 290

Now I'll make all changes to both modals and the subscriber simultaneously: 

Using "Multi-Replace String in Files"

Starting: *Update SonarrModal with nested dropdown* (4/5)

Now read the relevant sections of SonarrModal for exact string matching: 

Read [](file:///home/magiclegend/Documents/private/seerr/src/components/Settings/SonarrModal/index.tsx#240-240), lines 240 to 310

Using "Multi-Replace String in Files"

Starting: *Update MediaRequestSubscriber tagging logic* (5/5)

Now update the `MediaRequestSubscriber` tagging logic. I'll replace the `if (tagRequestsNoID) / else` blocks with a three-way switch for both Radarr and Sonarr: 

Using "Multi-Replace String in Files"

Completed: *Update MediaRequestSubscriber tagging logic* (5/5)

Let me quickly verify the changes look correct by checking for any remaining `tagRequestsNoID` references: 

Searched for text `tagRequestsNoID`, 14 results

There are locale files to update too. Let me update both: 

Read [](file:///home/magiclegend/Documents/private/seerr/src/i18n/locale/en.json#830-830), lines 830 to 840

Read [](file:///home/magiclegend/Documents/private/seerr/src/i18n/locale/en.json#1078-1078), lines 1078 to 1090

Read [](file:///home/magiclegend/Documents/private/seerr/src/i18n/locale/fr.json#1203-1203), lines 1203 to 1218

Using "Multi-Replace String in Files"

Now let me also update the fr.json for the `RadarrModal.tagRequestsInfo` entry that was left orphaned: 

Searched for text `tagRequestsInfo` (`**/src/i18n/locale/fr.json`), 2 results

Good — those are still correct and present. Let me do a final check to make sure there are no remaining `tagRequestsNoID` references outside of the migration file: 

Searched for text `tagRequestsNoID`, 6 results

All remaining references are only inside the migration file where they belong. Let me get a quick error check: 

Checked [](file:///home/magiclegend/Documents/private/seerr/server/lib/settings/index.ts), [](file:///home/magiclegend/Documents/private/seerr/server/subscriber/MediaRequestSubscriber.ts), [](file:///home/magiclegend/Documents/private/seerr/src/components/Settings/RadarrModal/index.tsx), [](file:///home/magiclegend/Documents/private/seerr/src/components/Settings/SonarrModal/index.tsx), no problems found

All clean. Here's a summary of every change made:

---

### Changes made

**index.ts**
- Replaced `tagRequestsNoID: boolean` with `tagRequestsFormat: 'userid' | 'userid-username' | 'username'` in `DVRSettings`

**0008_migrate_tag_requests_format.ts** *(new file)*
- Migrates existing settings: `tagRequestsNoID: true` → `tagRequestsFormat: 'username'`, `tagRequestsNoID: false` → `tagRequestsFormat: 'userid-username'`; deletes the old field

**index.tsx** & **index.tsx**
- Replaced old `tagRequestsNoID`/`tagRequestsNoIDInfo` messages with `tagRequestsFormat`, `tagFormatUserId`, `tagFormatUserIdUsername`, `tagFormatUsername`
- Initial value: `tagRequestsFormat: ..?.tagRequestsFormat ?? 'userid-username'`
- Submission: sends `tagRequestsFormat` instead of `tagRequestsNoID`
- UI: removed the separate `tagRequestsNoID` checkbox row; a `<select>` dropdown now appears **nested** (`pl-8`) directly under the `tagRequests` checkbox row, and is only visible when `tagRequests` is enabled — mirroring the child indentation pattern from `PermissionOption`

**MediaRequestSubscriber.ts**
- Replaced the `if (tagRequestsNoID) / else` boolean branch in both the Radarr and Sonarr tagging blocks with a unified three-way switch on `tagRequestsFormat`:
  - `'userid'` → tag is just the numeric user ID (e.g. `42`)
  - `'userid-username'` → tag is `{id}-{sanitizedDisplayName}` (previous default; legacy `id - name` tags still matched)
  - `'username'` → tag is the sanitised display name only

**en.json** & **fr.json**
- Replaced the old `tagRequestsNoID`/`tagRequestsNoIDInfo` locale keys with the four new keys (`tagRequestsFormat`, `tagFormatUserId`, `tagFormatUserIdUsername`, `tagFormatUsername`) for both Radarr and Sonarr namespaces

Made changes.

I then got it to change to an enum:

Prompt:

Ensure your changes match the code style of the rest of the file. Please implement the different options as an enum. The enum can be shared between the radarr and sonarr config. Find a logical place to put this enum, perhaps in lib/settings/index.ts?
Searched for regex `^export enum` (`**/server/lib/settings/index.ts`), 2 results

Read [](file:///home/magiclegend/Documents/private/seerr/server/lib/settings/index.ts#1-1), lines 1 to 90

Read [](file:///home/magiclegend/Documents/private/seerr/server/lib/settings/index.ts#100-100), lines 100 to 115

The style is clear: string-valued enums placed just before the interface that uses them. I'll add `TagRequestsFormat` right before `DVRSettings`, and update the field type and all usages. 

Using "Replace String in File"

Now update the subscriber and the migration file to use the enum, and update the modals to import and use it: 

Searched for text `tagRequestsFormat` (`**/server/subscriber/MediaRequestSubscriber.ts`), 2 results

Searched for regex `^import` (`**/server/subscriber/MediaRequestSubscriber.ts`), 18 results

Read [](file:///home/magiclegend/Documents/private/seerr/server/subscriber/MediaRequestSubscriber.ts#20-20), lines 20 to 32

Read [](file:///home/magiclegend/Documents/private/seerr/server/subscriber/MediaRequestSubscriber.ts#300-300), lines 300 to 675

Now I have all the context I need. I'll make all the changes in parallel: 

Using "Multi-Replace String in Files"

Let me check the current state of SonarrModal and the migration file: 

Searched for text `tagRequestsNoID\|tagRequestsFormat` (`**/src/components/Settings/SonarrModal/index.tsx`), no results

Searched for files matching `**/0008_migrate*`, 1 match

The migration file was named differently. Let me check the migrations directory and the current SonarrModal state: 

Read [](file:///home/magiclegend/Documents/private/seerr/server/lib/settings/migrations)

Read [](file:///home/magiclegend/Documents/private/seerr/src/components/Settings/SonarrModal/index.tsx#248-248), lines 248 to 260

Searched for text `tagRequests` (`**/src/components/Settings/SonarrModal/index.tsx`), 20 results

Read [](file:///home/magiclegend/Documents/private/seerr/server/lib/settings/migrations/0008_migrate_blacklist_to_blocklist.ts#1-1), lines 1 to 30

SonarrModal already has the import and submission right from the previous batch. Still need to update the default fallback string and the select option values. Let me also check the SonarrModal select options: 

Read [](file:///home/magiclegend/Documents/private/seerr/src/components/Settings/SonarrModal/index.tsx#1041-1041), lines 1041 to 1080

Now I'll fix the remaining raw strings in SonarrModal and create the migration as `0009`: 

Using "Multi-Replace String in Files"

Now create the migration with the correct number: 

Created [](file:///home/magiclegend/Documents/private/seerr/server/lib/settings/migrations/0009_migrate_tag_requests_format.ts)

Now verify there are no remaining raw string literals for the tag format values: 

Searched for regex `userid-username|'userid'|'username'`, 14 results

Checked [](file:///home/magiclegend/Documents/private/seerr/server/lib/settings/index.ts), [](file:///home/magiclegend/Documents/private/seerr/server/subscriber/MediaRequestSubscriber.ts), [](file:///home/magiclegend/Documents/private/seerr/src/components/Settings/RadarrModal/index.tsx), [](file:///home/magiclegend/Documents/private/seerr/src/components/Settings/SonarrModal/index.tsx), [](file:///home/magiclegend/Documents/private/seerr/server/lib/settings/migrations/0009_migrate_tag_requests_format.ts), no problems found

All clean. The remaining matches are all unrelated (the enum definition itself, comments, and unrelated `username` labels in other components).

Here's a summary of what changed:

**index.ts**
- Added `export enum TagRequestsFormat` with `USERID = 'userid'`, `USERID_USERNAME = 'userid-username'`, `USERNAME = 'username'` — placed directly before `DVRSettings`, matching the style of the existing `MetadataProviderType` enum
- Updated `tagRequestsFormat` field type from the inline union to `TagRequestsFormat`

**MediaRequestSubscriber.ts**
- Imported `TagRequestsFormat` alongside `getSettings`
- Replaced all string literal comparisons (`=== 'userid'`, etc.) and the fallback default (`?? 'userid-username'`) with `TagRequestsFormat.*` enum members in both the Radarr and Sonarr tagging blocks

**index.tsx** & **index.tsx**
- Imported `TagRequestsFormat` from `@server/lib/settings`
- Replaced the `?? 'userid-username'` default and all `value="..."` option attributes with `TagRequestsFormat.*` enum members

**0009_migrate_tag_requests_format.ts** *(renumbered from 0008 which was already taken)*
- Imports and uses `TagRequestsFormat` enum values instead of raw strings

Made changes.

Then I prompted for a refactor to better match the coding style of the rest of the file:

Prompt:

Do not create migrations, we are still developing.

Please go over your previous changes in MediaRequestSubscriber for the tagging logic, and ensure it is in the same style as the rest of the file.

Read [](file:///home/magiclegend/Documents/private/seerr/server/subscriber/MediaRequestSubscriber.ts#1-1), lines 1 to 60

Read [](file:///home/magiclegend/Documents/private/seerr/server/subscriber/MediaRequestSubscriber.ts#290-290), lines 290 to 380

Read [](file:///home/magiclegend/Documents/private/seerr/server/subscriber/MediaRequestSubscriber.ts#640-640), lines 640 to 760

Let me read the full context of both tagging blocks and also study the surrounding code style more carefully. 

Read [](file:///home/magiclegend/Documents/private/seerr/server/subscriber/MediaRequestSubscriber.ts#230-230), lines 230 to 290

Read [](file:///home/magiclegend/Documents/private/seerr/server/subscriber/MediaRequestSubscriber.ts#590-590), lines 590 to 720

Now I have a clear picture of the style. The file consistently:
- Initializes `let` variables with a value (never `let foo: string;` uninitialized)
- Overrides them with plain `if`/`else if` blocks — the same "initialize with default, override conditionally" pattern used throughout for `rootFolder`, `qualityProfile`, `tags`, etc.
- The original tagging code used `let userTag = radarrTags.find(...)` directly initialized with the legacy fallback, then `if (!userTag) { ... }` for the new format

The problems with the current code:
- `let tagLabel: string;` and `let tagStartsWith: string;` — explicit annotations on uninitialized variables, which never appear elsewhere
- `tagStartsWith` is an awkward abstraction that doesn't map clearly to any concept in the codebase
- The legacy fallback check is disjointed from the original pattern 

Using "Multi-Replace String in Files"

Read [](file:///home/magiclegend/Documents/private/seerr/server/subscriber/MediaRequestSubscriber.ts#302-302), lines 302 to 370

Read [](file:///home/magiclegend/Documents/private/seerr/server/subscriber/MediaRequestSubscriber.ts#653-653), lines 653 to 730

Using "Multi-Replace String in Files"


Checked [](file:///home/magiclegend/Documents/private/seerr/server/subscriber/MediaRequestSubscriber.ts), no problems found

The changes now mirror the file's established style throughout. Here's what changed and why each decision was made:

**Removed `let tagLabel: string` and `let tagStartsWith: string`** — nowhere else in the file are `let` variables declared uninitialized with explicit type annotations. Instead, `tagLabel` is now initialized to the `USERID_USERNAME` default value (matching the "initialize with default, override conditionally" pattern used for `rootFolder`, `qualityProfile`, and `tags` just above).

**Removed `tagStartsWith`** — it was an indirect abstraction that obscured intent. The find predicates are now written inline in each branch, exactly as the rest of the file handles conditional logic.

**Restored the original comment placement** — `// old tags had space around the hyphen` sits directly above the `let userTag` initialization (the legacy find), and `// new tags do not have spaces...` sits inside the `else if (!userTag)` block — identical to the original two-case structure.

**Structure now follows the `initialize → if/else if to override` pattern:**
- `let tagLabel = ...USERID_USERNAME default...` then `if (USERID)` / `else if (USERNAME)` to override it
- `let userTag = ...legacy find...` then `if (USERID)` / `else if (USERNAME)` to override it, or `else if (!userTag)` to do the new-format fallback for `USERID_USERNAME`
- Shared create/add/warn block unchanged at the end

Made changes.

How Has This Been Tested?

I tested this by connecting the development instance running with pnpm to my 'production' Radarr and Sonarr instances. Then, I tried all three configurations and confirmed that the tag was either created or set correctly. I also validated the logic by attaching a Node debugger to MediaRequestSubscriber.ts to verify the logic.

Node Debugger stuff in case somebody is interested

Got Claude Sonnet 4.5 to figure out the configs, so they are indeed garbage, hence I'm not committing any of it. But they do work. Would be great if somebody who is more familiar with these tools could tell me which settings are indeed required for debugging and source maps so this can be documented properly.

package.json:

Added a debug command to start in development mode with the debug server:

"scripts": {
  "debug": "NODE_ENV=development TS_NODE_PROJECT=server/tsconfig.json nodemon -e ts --watch server --watch seerr-api.yml -e .json,.ts,.yml --exec \"node --inspect=9229 -r ts-node/register -r tsconfig-paths/register\" server/index.ts"
}

Then, created a launch.json:

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "attach",
      "name": "Attach to Debug Server",
      "port": 9229,
      "restart": true,
      "skipFiles": ["<node_internals>/**", "**/node_modules/**"],
      "protocol": "inspector",
      "sourceMaps": true,
      "localRoot": "${workspaceFolder}",
      "remoteRoot": "${workspaceFolder}",
      "resolveSourceMapLocations": [
        "${workspaceFolder}/**",
        "!**/node_modules/**"
      ],
      "console": "integratedTerminal",
      "internalConsoleOptions": "neverOpen"
    }
  ]
}

This should be enough to get a debugger working. Try placing a breakpoint in server/index.ts at line 1 or 2 and it should bind (= become red).

Then, to also get React Devtools working I added configs for sourcemapping:

tsconfig.json, add:

{
  "compilerOptions": {
    "sourceMap": true
   }
}

server/tsconfig.json, replace:

{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "outDir": "../dist",
    "noEmit": false,
    "incremental": true,
    "baseUrl": ".",
    "sourceMap": false,
    "inlineSourceMap": true,
    "inlineSources": true,
    "paths": {
      "@server/*": ["*"]
    }
  },
  "include": ["**/*.ts"],
  "ts-node": {
    "files": true,
    "transpileOnly": false,
    "compilerOptions": {
      "sourceMap": false,
      "inlineSourceMap": true,
      "inlineSources": true
    }
  }
}

No, not all settings are probably needed, but I couldn't be arsed with figuring out which ones did the trick, I just wanted it to work.

Screenshots / Logs (if applicable)

No dropdown if the tag option is disabled:
image

Added dropdown:
image

Result from tests:

Old tags with spaces:
image

Just username tag:
image

Just ID:
image

Checklist:

  • I have read and followed the contribution guidelines.
  • Disclosed any use of AI (see our policy)
  • I have updated the documentation accordingly. -> Should this behaviour be documented?
  • All new and existing tests passed.
  • Successful build pnpm build
  • Translation keys pnpm i18n:extract
  • Database migration (if required)

Summary by CodeRabbit

  • New Features

    • Added a Tag Requests Format option in Radarr and Sonarr settings (selectable when tag requests are enabled); choice is saved and affects how request tags are generated and matched.
    • Improved tag handling for Radarr/Sonarr to support three formatting modes and a new default label style.
    • Added "Test connection to load tags" action in Radarr and Sonarr settings.
  • Localization

    • Added language strings for the three tag format options and clarified tag request informational text.

nodiaque and others added 6 commits February 18, 2026 13:13
…r to tag without ID

The current feature allow to add a custom tag "userid-username" upon request to either radarr ou
sonarr. With this new feature, when Tag Request is enabled and this option is also enable, the tag
will only be "username" instead of "userid-username"

seerr-team#727
…sed tag format

Created a dropdown to give the user control of what tag style is used. This change means its no
longer mandatory to have the internal userID in the tag. The dropdown is also automatically hidden
if the tagging is disabled to decrease UI clutter. This commit refactors work from commit bbf0f06.

Resolves issue seerr-team#727
Builds on and obsoletes PR seerr-team#2282
…tag formatting options

Changed the hard-coded strings to an enum to decrease the possibility of spelling mistakes causing
broken code.
@MagicLegend MagicLegend requested a review from a team as a code owner February 18, 2026 15:07
@MagicLegend MagicLegend changed the title Feat: Tag formatting options feat: Tag formatting options Feb 18, 2026
@MagicLegend MagicLegend force-pushed the tag-formatting-options branch from 1d9e817 to 86e98dc Compare February 18, 2026 15:08
@coderabbitai
Copy link

coderabbitai bot commented Feb 18, 2026

No actionable comments were generated in the recent review. 🎉


📝 Walkthrough

Walkthrough

Adds an exported enum TagRequestsFormat, persists it in DVR settings, surfaces a selectable format in Radarr/Sonarr settings UI, and updates the media request subscriber to compute and apply request tags in one of three configured formats.

Changes

Cohort / File(s) Summary
Constants & Settings
server/constants/server.ts, server/lib/settings/index.ts
Add exported TagRequestsFormat enum (userid, userid-username, username) and add/re-export tagRequestsFormat: TagRequestsFormat on DVRSettings.
Backend: tag application
server/subscriber/MediaRequestSubscriber.ts
Compute a single tagLabel based on configured format; update lookup, creation, and association flows for Radarr and Sonarr to use the new label logic while preserving legacy fallback.
Frontend: settings UI
src/components/Settings/RadarrModal/index.tsx, src/components/Settings/SonarrModal/index.tsx
Add tagRequestsFormat to form initialValues and save payload; render conditional select with three format options and new i18n keys; tweak informational text.
Localization
src/i18n/locale/en.json
Add locale keys for tag format labels and tagRequestsFormat for both modals; update tagRequestsInfo wording and add test hook keys.

Sequence Diagram(s)

sequenceDiagram
    actor Admin
    participant UI as RadarrModal/SonarrModal
    participant API as Settings API
    participant Config as DVRSettings
    participant Subscriber as MediaRequestSubscriber
    participant Arr as Arr API

    Admin->>UI: Choose tagRequestsFormat
    UI->>API: Save settings (include tagRequestsFormat)
    API->>Config: Persist tagRequestsFormat

    Arr->>Subscriber: New media request event
    Subscriber->>Config: Read tagRequestsFormat
    alt userid
        Subscriber->>Subscriber: tagLabel = "123"
    else userid-username
        Subscriber->>Subscriber: tagLabel = "123-tomjones"
    else username
        Subscriber->>Subscriber: tagLabel = "tomjones"
    end
    Subscriber->>Arr: create/update tag (`tagLabel`) and attach to request
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped through enums and tags today,
Three little formats to show the way.
IDs, names, or hyphen in tune,
Admins pick the tag that fits the room.
I munch code, then trail-hop away.

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning All changes directly support tag formatting configurability: enum definition, settings extension, UI implementation, and subscriber logic updates are all in-scope; however, inclusion of French translation changes appears out-of-scope per maintainer feedback that translations should be handled via Weblate. Remove French translation changes (fr.json updates) from this PR as translations are managed separately via Weblate; only English locale changes are appropriate for code PRs.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: Tag formatting options' directly matches the main changeset objective of adding configurable tag format selection for request tagging.
Linked Issues check ✅ Passed The PR implements configurable tag formatting (USERID, USERID_USERNAME, USERNAME) via enum, settings UI dropdown, and subscriber logic that directly satisfies issue #727's core objective of allowing administrators to control user information formatting in tags.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/i18n/locale/fr.json (1)

1204-1213: ⚠️ Potential issue | 🟡 Minor

French locale still uses legacy tag keys and misses new tag-format strings.

The UI now references tagRequestsFormat + tagFormatUserId/UserIdUsername/Username, but fr.json only adds the old tagRequestsNoID keys and keeps the old info text. This will surface missing keys in French and misdescribe the feature.

✏️ Suggested i18n updates
-  "components.Settings.RadarrModal.tagRequestsNoID": "Pas de ID dans le tag",
-  "components.Settings.RadarrModal.tagRequestsNoIDInfo": "Ne pas ajouter le préfixe ID utilisateur au tag. Requis que l'option Tager les demandes soit active.",
+  "components.Settings.RadarrModal.tagRequestsFormat": "Format du tag",
+  "components.Settings.RadarrModal.tagFormatUserId": "ID utilisateur",
+  "components.Settings.RadarrModal.tagFormatUserIdUsername": "ID utilisateur - Nom d'utilisateur",
+  "components.Settings.RadarrModal.tagFormatUsername": "Nom d'utilisateur",
-  "components.Settings.SonarrModal.tagRequestsNoID": "Pas de ID dans le tag",
-  "components.Settings.SonarrModal.tagRequestsNoIDInfo": "Ne pas ajouter le préfixe ID utilisateur au tag. Requis que l'option Tager les demandes soit active.",
+  "components.Settings.SonarrModal.tagRequestsFormat": "Format du tag",
+  "components.Settings.SonarrModal.tagFormatUserId": "ID utilisateur",
+  "components.Settings.SonarrModal.tagFormatUserIdUsername": "ID utilisateur - Nom d'utilisateur",
+  "components.Settings.SonarrModal.tagFormatUsername": "Nom d'utilisateur",
-  "components.Settings.SonarrModal.tagRequestsInfo": "Ajouter automatiquement un tag supplémentaire avec l'identifiant et le nom du demandeur",
+  "components.Settings.SonarrModal.tagRequestsInfo": "Ajouter automatiquement un tag supplémentaire avec l'identifiant utilisateur et/ou le nom d'affichage du demandeur",
-  "components.Settings.RadarrModal.tagRequestsInfo": "Ajouter automatiquement un tag supplémentaire avec l'identifiant et le nom du demandeur",
+  "components.Settings.RadarrModal.tagRequestsInfo": "Ajouter automatiquement un tag supplémentaire avec l'identifiant utilisateur et/ou le nom d'affichage du demandeur",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/i18n/locale/fr.json` around lines 1204 - 1213, The French locale is
missing the new tag-format keys and still uses legacy keys; add translations for
the new keys used by the UI: components.Settings.RadarrModal.tagRequestsFormat,
components.Settings.RadarrModal.tagFormatUserId,
components.Settings.RadarrModal.tagFormatUserIdUsername,
components.Settings.RadarrModal.tagFormatUsername (and the corresponding
components.Settings.SonarrModal.* keys), and update
components.Settings.*.tagRequestsInfo to reflect the new format text (replace
the old "tagRequestsNoID"/"tagRequestsNoIDInfo" wording). Ensure each new key
has an accurate French string equivalent to the English originals so the UI no
longer shows missing keys or incorrect descriptions.
🧹 Nitpick comments (1)
server/subscriber/MediaRequestSubscriber.ts (1)

652-687: Consider extracting tag handling logic to reduce duplication.

The tag format handling logic at lines 652-687 is nearly identical to the Radarr logic at lines 306-341. While the current implementation is correct and follows the existing pattern in this file, consider extracting this into a shared helper method to reduce duplication.

♻️ Suggested helper extraction
private async resolveOrCreateUserTag(
  api: RadarrAPI | SonarrAPI,
  tagFormat: TagRequestsFormat,
  requestedBy: { id: number; displayName: string },
  entity: MediaRequest
): Promise<number | undefined> {
  const tags = await api.getTags();
  
  let tagLabel = requestedBy.id + '-' + sanitizeDisplayName(requestedBy.displayName);
  let userTag = tags.find((v) => v.label.startsWith(requestedBy.id + '-'));
  
  if (tagFormat === TagRequestsFormat.USERID) {
    tagLabel = String(requestedBy.id);
    userTag = tags.find((v) => v.label === tagLabel);
  } else if (tagFormat === TagRequestsFormat.USERNAME) {
    tagLabel = sanitizeDisplayName(requestedBy.displayName.toLowerCase());
    userTag = tags.find((v) => v.label === tagLabel);
  } else if (!userTag) {
    userTag = tags.find((v) => v.label.startsWith(requestedBy.id + ' - '));
  }
  
  if (!userTag) {
    userTag = await api.createTag({ label: tagLabel });
  }
  
  return userTag?.id;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/subscriber/MediaRequestSubscriber.ts` around lines 652 - 687, Extract
the duplicated Sonarr/Radarr tag-resolution and creation logic in
MediaRequestSubscriber.ts into a shared helper (e.g., resolveOrCreateUserTag)
that accepts the API client (RadarrAPI | SonarrAPI), TagRequestsFormat,
requestedBy ({id, displayName}) and the MediaRequest entity, then moves the tag
label construction, lookups (startsWith id+'-' and legacy ' - '), format
branches (USERID, USERNAME) and createTag call into that helper and return the
resolved tag id; replace the in-place logic in the Sonarr block (the code
handling tagFormat, tagLabel, userTag, and createTag) with a call to this helper
to remove duplication with the Radarr block.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/i18n/locale/fr.json`:
- Around line 1204-1213: The French locale is missing the new tag-format keys
and still uses legacy keys; add translations for the new keys used by the UI:
components.Settings.RadarrModal.tagRequestsFormat,
components.Settings.RadarrModal.tagFormatUserId,
components.Settings.RadarrModal.tagFormatUserIdUsername,
components.Settings.RadarrModal.tagFormatUsername (and the corresponding
components.Settings.SonarrModal.* keys), and update
components.Settings.*.tagRequestsInfo to reflect the new format text (replace
the old "tagRequestsNoID"/"tagRequestsNoIDInfo" wording). Ensure each new key
has an accurate French string equivalent to the English originals so the UI no
longer shows missing keys or incorrect descriptions.

---

Nitpick comments:
In `@server/subscriber/MediaRequestSubscriber.ts`:
- Around line 652-687: Extract the duplicated Sonarr/Radarr tag-resolution and
creation logic in MediaRequestSubscriber.ts into a shared helper (e.g.,
resolveOrCreateUserTag) that accepts the API client (RadarrAPI | SonarrAPI),
TagRequestsFormat, requestedBy ({id, displayName}) and the MediaRequest entity,
then moves the tag label construction, lookups (startsWith id+'-' and legacy ' -
'), format branches (USERID, USERNAME) and createTag call into that helper and
return the resolved tag id; replace the in-place logic in the Sonarr block (the
code handling tagFormat, tagLabel, userTag, and createTag) with a call to this
helper to remove duplication with the Radarr block.

@MagicLegend
Copy link
Author

MagicLegend commented Feb 18, 2026

Ah, Coderabbit is of course rightfully mad about the French translation now. Is there maybe a French speaker who can confirm the AI translations Coderabbit gave?

I also agree with the code duplication nitpick, but since this seemed to be the pattern in this file I ended up duplicating the code anyway.

@nodiaque
Copy link

I speak french, what do you need?

Copy link
Member

@gauthier-th gauthier-th left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also please change the description of User ID - Username to UserID-Username to avoid confusion with spaces in tags.

@gauthier-th
Copy link
Member

I speak french, what do you need?

Translations (other than English) don't belong to the PR. As said above, they are handled by our Weblate.

@MagicLegend MagicLegend force-pushed the tag-formatting-options branch 2 times, most recently from 3450dcd to 11670fa Compare February 18, 2026 15:32
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
server/subscriber/MediaRequestSubscriber.ts (1)

304-356: Consider extracting shared tagging logic.

The Radarr (lines 304-356) and Sonarr (lines 650-702) tagging blocks are nearly identical. While the PR author noted following existing patterns, a helper function could reduce duplication and ensure consistent behavior across both integrations.

This is not blocking since the current implementation works correctly.

Example helper extraction
async function resolveOrCreateUserTag(
  api: RadarrAPI | SonarrAPI,
  tagFormat: TagRequestsFormat,
  userId: number,
  displayName: string
): Promise<{ id?: number; label: string }> {
  const tags = await api.getTags();
  const sanitized = sanitizeDisplayName(displayName.toLowerCase());
  
  let tagLabel: string;
  let userTag: typeof tags[number] | undefined;
  
  switch (tagFormat) {
    case TagRequestsFormat.USERID:
      tagLabel = String(userId);
      userTag = tags.find((v) => v.label === tagLabel);
      break;
    case TagRequestsFormat.USERNAME:
      tagLabel = sanitized;
      userTag = tags.find((v) => v.label === tagLabel);
      break;
    default: // USERID_USERNAME
      tagLabel = `${userId}-${sanitized}`;
      userTag = tags.find((v) => v.label.startsWith(`${userId}-`)) 
             ?? tags.find((v) => v.label.startsWith(`${userId} - `));
      break;
  }
  
  return userTag ?? await api.createTag({ label: tagLabel });
}

Also applies to: 650-702

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/subscriber/MediaRequestSubscriber.ts` around lines 304 - 356, The
repeated tagging logic in MediaRequestSubscriber.ts (the Radarr block around the
Radarr tag handling and the Sonarr block at lines ~650-702) should be extracted
into a shared helper (e.g., resolveOrCreateUserTag) that accepts the API client
(RadarrAPI|SonarrAPI), TagRequestsFormat, userId, displayName and returns the
resolved/created tag object/label; replace both inline blocks with calls to that
helper and then push the returned tag.id into the existing tags array if
present, preserving existing logging and radarr/sonarr.createTag behavior and
types used by functions like radarr.getTags()/createTag and sanitizeDisplayName
so imports and types align.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/subscriber/MediaRequestSubscriber.ts`:
- Around line 310-326: The tag casing is inconsistent: when tagFormat is
TagRequestsFormat.USERNAME the code lowercases entity.requestedBy.displayName
before calling sanitizeDisplayName but for the USERID_USERNAME branch it does
not, producing mixed-case tags; update the USERID_USERNAME construction (the
tagLabel and the radarrTags lookup for userTag) to lowercase the display name
before sanitization just like the USERNAME branch (apply to variables tagLabel,
userTag, sanitizeDisplayName and the radarrTags.find calls that use
entity.requestedBy.displayName) so both formats produce lowercase sanitized
tags.

---

Nitpick comments:
In `@server/subscriber/MediaRequestSubscriber.ts`:
- Around line 304-356: The repeated tagging logic in MediaRequestSubscriber.ts
(the Radarr block around the Radarr tag handling and the Sonarr block at lines
~650-702) should be extracted into a shared helper (e.g.,
resolveOrCreateUserTag) that accepts the API client (RadarrAPI|SonarrAPI),
TagRequestsFormat, userId, displayName and returns the resolved/created tag
object/label; replace both inline blocks with calls to that helper and then push
the returned tag.id into the existing tags array if present, preserving existing
logging and radarr/sonarr.createTag behavior and types used by functions like
radarr.getTags()/createTag and sanitizeDisplayName so imports and types align.

Comment on lines +310 to +326
let tagLabel =
entity.requestedBy.id +
'-' +
sanitizeDisplayName(entity.requestedBy.displayName);
// new tags do not have spaces around the hyphen, since spaces are not allowed anymore
// note: this can theoretically collide if a USERNAME-format tag starts with this user's id
let userTag = radarrTags.find((v) =>
v.label.startsWith(entity.requestedBy.id + ' - ')
v.label.startsWith(entity.requestedBy.id + '-')
);
// new tags do not have spaces around the hyphen, since spaces are not allowed anymore
if (!userTag) {
if (tagFormat === TagRequestsFormat.USERID) {
tagLabel = String(entity.requestedBy.id);
userTag = radarrTags.find((v) => v.label === tagLabel);
} else if (tagFormat === TagRequestsFormat.USERNAME) {
tagLabel = sanitizeDisplayName(
entity.requestedBy.displayName.toLowerCase()
);
userTag = radarrTags.find((v) => v.label === tagLabel);
Copy link

@coderabbitai coderabbitai bot Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistent case handling between tag formats.

For USERNAME format (line 323-324), the display name is lowercased before sanitization. For USERID_USERNAME format (line 310-313), it is not. This creates inconsistent tag casing:

  • USERID_USERNAME1-MagicLegend
  • USERNAMEmagiclegend

If consistent lowercase tags are desired (as shown in PR screenshots), consider lowercasing the display name for USERID_USERNAME as well:

Proposed fix for consistent lowercase
          let tagLabel =
            entity.requestedBy.id +
            '-' +
-           sanitizeDisplayName(entity.requestedBy.displayName);
+           sanitizeDisplayName(entity.requestedBy.displayName.toLowerCase());
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let tagLabel =
entity.requestedBy.id +
'-' +
sanitizeDisplayName(entity.requestedBy.displayName);
// new tags do not have spaces around the hyphen, since spaces are not allowed anymore
// note: this can theoretically collide if a USERNAME-format tag starts with this user's id
let userTag = radarrTags.find((v) =>
v.label.startsWith(entity.requestedBy.id + ' - ')
v.label.startsWith(entity.requestedBy.id + '-')
);
// new tags do not have spaces around the hyphen, since spaces are not allowed anymore
if (!userTag) {
if (tagFormat === TagRequestsFormat.USERID) {
tagLabel = String(entity.requestedBy.id);
userTag = radarrTags.find((v) => v.label === tagLabel);
} else if (tagFormat === TagRequestsFormat.USERNAME) {
tagLabel = sanitizeDisplayName(
entity.requestedBy.displayName.toLowerCase()
);
userTag = radarrTags.find((v) => v.label === tagLabel);
let tagLabel =
entity.requestedBy.id +
'-' +
sanitizeDisplayName(entity.requestedBy.displayName.toLowerCase());
// new tags do not have spaces around the hyphen, since spaces are not allowed anymore
// note: this can theoretically collide if a USERNAME-format tag starts with this user's id
let userTag = radarrTags.find((v) =>
v.label.startsWith(entity.requestedBy.id + '-')
);
if (tagFormat === TagRequestsFormat.USERID) {
tagLabel = String(entity.requestedBy.id);
userTag = radarrTags.find((v) => v.label === tagLabel);
} else if (tagFormat === TagRequestsFormat.USERNAME) {
tagLabel = sanitizeDisplayName(
entity.requestedBy.displayName.toLowerCase()
);
userTag = radarrTags.find((v) => v.label === tagLabel);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/subscriber/MediaRequestSubscriber.ts` around lines 310 - 326, The tag
casing is inconsistent: when tagFormat is TagRequestsFormat.USERNAME the code
lowercases entity.requestedBy.displayName before calling sanitizeDisplayName but
for the USERID_USERNAME branch it does not, producing mixed-case tags; update
the USERID_USERNAME construction (the tagLabel and the radarrTags lookup for
userTag) to lowercase the display name before sanitization just like the
USERNAME branch (apply to variables tagLabel, userTag, sanitizeDisplayName and
the radarrTags.find calls that use entity.requestedBy.displayName) so both
formats produce lowercase sanitized tags.

✅ Addressed in commits 21eda41 to 6385feb

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah crap, this is correct. I missed this because the 1 - magiclegend label already existed on my *arrs, so it didn't create a new one.

@gauthier-th Would the preference be to keep the user-supplied capitalization, or to standardize it to lowercase? Personally I would do the latter to ensure consistency.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

@MagicLegend MagicLegend force-pushed the tag-formatting-options branch from 11670fa to 6385feb Compare February 18, 2026 15:43
@MagicLegend
Copy link
Author

Also please change the description of User ID - Username to UserID-Username to avoid confusion with spaces in tags.

I considered this as well, but ended up not doing it for readability. I've changed the labels. I've also updated the User ID one to UserID to match the style of the other label. I just pushed this change.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
server/subscriber/MediaRequestSubscriber.ts (1)

304-356: Consider extracting shared tag-handling logic.

The tagging logic for Radarr (lines 304-356) and Sonarr (lines 650-702) is nearly identical. A shared helper function could reduce duplication and ensure both flows stay in sync.

That said, I acknowledge your PR comment about matching existing file patterns—this is a reasonable tradeoff and can be addressed in a future cleanup.

Example helper extraction
interface TagHandler {
  getTags: () => Promise<{ id: number; label: string }[]>;
  createTag: (options: { label: string }) => Promise<{ id: number; label: string }>;
}

async function resolveUserTag(
  handler: TagHandler,
  tagFormat: TagRequestsFormat,
  userId: number,
  displayName: string
): Promise<{ id: number; label: string } | undefined> {
  const tags = await handler.getTags();
  const sanitizedName = sanitizeDisplayName(displayName.toLowerCase());
  
  let tagLabel: string;
  let userTag: { id: number; label: string } | undefined;

  switch (tagFormat) {
    case TagRequestsFormat.USERID:
      tagLabel = String(userId);
      userTag = tags.find((v) => v.label === tagLabel);
      break;
    case TagRequestsFormat.USERNAME:
      tagLabel = sanitizedName;
      userTag = tags.find((v) => v.label === tagLabel);
      break;
    default: // USERID_USERNAME
      tagLabel = `${userId}-${sanitizedName}`;
      userTag = tags.find((v) => v.label.startsWith(`${userId}-`)) 
             ?? tags.find((v) => v.label.startsWith(`${userId} - `));
      break;
  }

  if (!userTag) {
    userTag = await handler.createTag({ label: tagLabel });
  }

  return userTag;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/subscriber/MediaRequestSubscriber.ts` around lines 304 - 356, Extract
the duplicated tag resolution/creation logic into a shared helper (e.g.,
resolveUserTag) that accepts an adapter implementing a small TagHandler
interface (methods: getTags and createTag), the TagRequestsFormat, userId and
displayName, and returns the resolved tag object or undefined; replace the
Radarr block (radarr.getTags, radarr.createTag, tagLabel/userTag logic) and the
Sonarr block with calls to this helper, reusing existing utilities
TagRequestsFormat and sanitizeDisplayName to build labels and to preserve the
old-space fallback (startsWith(userId + ' - ')) for USERID_USERNAME mode. Ensure
callers push the returned tag.id into their tags array and keep the same logging
paths (logger.info/warn) when createTag fails.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@server/subscriber/MediaRequestSubscriber.ts`:
- Around line 310-326: The tag generation is inconsistent: in the branch that
builds USERID_USERNAME tags it calls
sanitizeDisplayName(entity.requestedBy.displayName) without lowercasing, while
the USERNAME branch lowercases before sanitizing; update the USERID_USERNAME
branch so tagLabel uses
sanitizeDisplayName(entity.requestedBy.displayName.toLowerCase()) and ensure
userTag lookup uses the same lowercased/sanitized label (references: tagLabel,
userTag, sanitizeDisplayName, and TagRequestsFormat.USERNAME/USERID_USERNAME) so
all display-name-based tags are consistently lowercase.
- Around line 656-672: The Sonarr tag-label logic in MediaRequestSubscriber.ts
is inconsistent: when tagFormat === TagRequestsFormat.USERID_USERNAME you build
tagLabel using sanitizeDisplayName(entity.requestedBy.displayName) (not
lowercased) but when tagFormat === TagRequestsFormat.USERNAME you use
sanitizeDisplayName(entity.requestedBy.displayName.toLowerCase()), causing case
mismatches; update the USERID_USERNAME branch to lowercase the display name
before sanitizing (use the same .toLowerCase() call used in the USERNAME branch)
and ensure the userTag lookups (sonarrTags.find) compare against the
normalized/lowercased sanitized label consistently so sanitizeDisplayName and
TagRequestsFormat branches produce matching case.

---

Nitpick comments:
In `@server/subscriber/MediaRequestSubscriber.ts`:
- Around line 304-356: Extract the duplicated tag resolution/creation logic into
a shared helper (e.g., resolveUserTag) that accepts an adapter implementing a
small TagHandler interface (methods: getTags and createTag), the
TagRequestsFormat, userId and displayName, and returns the resolved tag object
or undefined; replace the Radarr block (radarr.getTags, radarr.createTag,
tagLabel/userTag logic) and the Sonarr block with calls to this helper, reusing
existing utilities TagRequestsFormat and sanitizeDisplayName to build labels and
to preserve the old-space fallback (startsWith(userId + ' - ')) for
USERID_USERNAME mode. Ensure callers push the returned tag.id into their tags
array and keep the same logging paths (logger.info/warn) when createTag fails.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Per user tag added to sonarr & radarr includes user id number

3 participants

Comments