Skip to content

feat: Tag formatting options - #2504

Open
MagicLegend wants to merge 11 commits into
seerr-team:developfrom
MagicLegend:tag-formatting-options
Open

MagicLegend wants to merge 11 commits into
seerr-team:developfrom
MagicLegend:tag-formatting-options

Conversation

@MagicLegend

@MagicLegend MagicLegend commented Feb 18, 2026 •

Copy link
Copy Markdown
Contributor

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 (shown when tag requests are enabled); choice is saved and changes how request tags are generated and matched. Default set to "User ID + Username".
    • 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.

@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

coderabbitai Bot commented Feb 18, 2026 •

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds an exported enum TagRequestsFormat, persists it on DVR settings, exposes 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 (userid, userid-username, username).

Changes

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

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through enums and tags today,
Three tiny formats to lead the way.
ID, name, or dashed combine,
Admins choose how the labels align.
I nibble code and thump—hooray!

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning All changes directly support the tag formatting feature. However, the PR includes French translation updates (fr.json) that should not be in scope per reviewer feedback that translations are handled via Weblate. Remove French translation changes from the PR; keep only English translations in src/i18n/locale/en.json as translations are managed externally via Weblate.
✅ 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' accurately summarizes the main change: introducing configurable tag formatting for request tags.
Linked Issues check ✅ Passed The PR implements core requirements from #727: allows administrators to choose between USERID, USERID_USERNAME, and USERNAME formats, with proper tag handling for all three options.

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


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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

MagicLegend commented Feb 18, 2026 •

Copy link
Copy Markdown
Contributor Author

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.

Comment thread src/i18n/locale/fr.json
@nodiaque

Copy link
Copy Markdown

I speak french, what do you need?

@gauthier-th gauthier-th left a comment

Copy link
Copy Markdown
Member

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
Copy Markdown
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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 thread server/subscriber/MediaRequestSubscriber.ts
@MagicLegend
MagicLegend force-pushed the tag-formatting-options branch from 11670fa to 6385feb Compare February 18, 2026 15:43
@MagicLegend

Copy link
Copy Markdown
Contributor 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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.

@MagicLegend
MagicLegend force-pushed the tag-formatting-options branch from 76f2364 to 2bf7980 Compare February 19, 2026 13:18

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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/lib/settings/index.ts`:
- Around line 79-81: The settings schema currently drops legacy boolean
tagRequestsNoID and defaults tagRequestsFormat to USERID_USERNAME, causing
installs that previously had tagRequestsNoID=true to revert; add a migration or
load-time normalization that maps tagRequestsNoID → tagRequestsFormat (if
tagRequestsNoID === true set tagRequestsFormat = TagRequestsFormat.USERNAME, if
false set TagRequestsFormat.USERID_USERNAME) before the rest of the app reads
settings, update the settings loader/initializer (where tagRequestsFormat is
read) to apply this mapping and consider making tagRequestsFormat optional in
the Settings interface until migration runs so the runtime accurately reflects
legacy state.

Comment thread server/lib/settings/index.ts
@MagicLegend
MagicLegend force-pushed the tag-formatting-options branch from 2bf7980 to f966dce Compare February 24, 2026 08:26
@MagicLegend

Copy link
Copy Markdown
Contributor Author

This PR is waiting on a reply about the following:

⚠️ 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_USERNAME → 1-MagicLegend
  • USERNAME → magiclegend

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.

🤖 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

@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.

I'd like to get this merged to get this in the next release, I'm waiting on this feature :)

@gauthier-th

Copy link
Copy Markdown
Member

@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.

We always used capitalized usernames. I don't think we should change it. Admins/users will decide if they want uppercase in the usernames.

@MagicLegend
MagicLegend force-pushed the tag-formatting-options branch 2 times, most recently from e179a3d to 1cd5905 Compare February 24, 2026 12:56
@MagicLegend

Copy link
Copy Markdown
Contributor Author

@gauthier-th Gotcha! I've removed the .toLowerCase() to keep the user-supplied capitalization.

Is there anything else needed from my side to get this merged?

@gauthier-th

Copy link
Copy Markdown
Member

Is there anything else needed from my side to get this merged?

Patience 🙂

@github-actions github-actions Bot added the merge conflict Cannot merge due to merge conflicts label Feb 27, 2026
@github-actions

Copy link
Copy Markdown

This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.

@DrummyFloyd

DrummyFloyd commented Feb 28, 2026 •

Copy link
Copy Markdown

if this change appears in main, should we expect a sync method to push new format and remove old one (based on old requests)?

…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.
Copilot AI review requested due to automatic review settings July 22, 2026 07:39
@MagicLegend
MagicLegend force-pushed the tag-formatting-options branch from d2dcfb5 to b9f1227 Compare July 22, 2026 07:39
@github-actions github-actions Bot removed the merge conflict Cannot merge due to merge conflicts label Jul 22, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds configurable tag formatting for Radarr/Sonarr “Tag Requests”, allowing admins to choose whether requester tags are created as user id only, username only, or id-username, and updates the subscriber logic accordingly.

Changes:

  • Introduces TagRequestsFormat enum and persists a tagRequestsFormat setting alongside tagRequests.
  • Updates Radarr/Sonarr settings modals to show a nested “Tag Format” dropdown when “Tag Requests” is enabled.
  • Updates MediaRequestSubscriber to create/find tags according to the selected format (with legacy tag matching support).

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/i18n/locale/en.json Adds new locale keys for the tag format dropdown and updates tag request help text.
src/components/Settings/SonarrModal/index.tsx Adds tag format dropdown UI and submits tagRequestsFormat.
src/components/Settings/RadarrModal/index.tsx Adds tag format dropdown UI and submits tagRequestsFormat.
server/subscriber/MediaRequestSubscriber.ts Updates Radarr/Sonarr tag matching/creation logic to honor the configured format.
server/lib/settings/index.ts Adds tagRequestsFormat to DVR settings typing and re-exports the enum.
server/constants/server.ts Defines TagRequestsFormat enum values.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/i18n/locale/en.json
Comment thread src/i18n/locale/en.json
Comment thread server/lib/settings/index.ts
Comment thread server/subscriber/MediaRequestSubscriber.ts
Comment thread server/subscriber/MediaRequestSubscriber.ts
@github-actions github-actions Bot removed the stale label Jul 22, 2026
Copilot AI review requested due to automatic review settings July 22, 2026 07:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (3)

server/lib/settings/index.ts:87

  • tagRequestsFormat is a newly introduced setting and appears to be optional in practice (no migration + existing stored Radarr/Sonarr instances may not have it). Keeping it required in DVRSettings makes the type system assume it always exists, which can hide runtime undefined reads elsewhere.
  tagRequestsFormat: TagRequestsFormat;

server/subscriber/MediaRequestSubscriber.ts:318

  • In the USERID_USERNAME path, the initial lookup uses startsWith("<id>-" ), which can incorrectly match a USERNAME-format tag when the (sanitized) username itself starts with "-" (the file comment already notes this collision case). Excluding the username-only label from the prefix match avoids picking the wrong tag in that scenario.
          // 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 + '-')
          );

server/subscriber/MediaRequestSubscriber.ts:680

  • Same collision risk as the Radarr block: the USERID_USERNAME lookup uses a broad startsWith("<id>-" ) match that can accidentally pick a USERNAME-format tag if the username begins with "-". Excluding the username-only label from the prefix match prevents that wrong-tag selection.
          // 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 = sonarrTags.find((v) =>
            v.label.startsWith(entity.requestedBy.id + '-')
          );

Comment thread src/i18n/locale/en.json
Comment on lines +882 to +884
"components.Settings.RadarrModal.tagFormatUserId": "UserID",
"components.Settings.RadarrModal.tagFormatUserIdUsername": "UserID-Username",
"components.Settings.RadarrModal.tagFormatUsername": "Username",
Comment thread src/i18n/locale/en.json
Comment on lines +1152 to +1154
"components.Settings.SonarrModal.tagFormatUserId": "UserID",
"components.Settings.SonarrModal.tagFormatUserIdUsername": "UserID-Username",
"components.Settings.SonarrModal.tagFormatUsername": "Username",
Comment on lines +69 to +72
tagRequestsFormat: 'Tag Format',
tagFormatUserId: 'UserID',
tagFormatUserIdUsername: 'UserID-Username',
tagFormatUsername: 'Username',
Comment on lines +76 to +79
tagRequestsFormat: 'Tag Format',
tagFormatUserId: 'UserID',
tagFormatUserIdUsername: 'UserID-Username',
tagFormatUsername: 'Username',
@MagicLegend

Copy link
Copy Markdown
Contributor Author

Yes. But AFAIK, this never caused any issue in years, and modifying this existing behavior now would require migrating all existing tags. If a conflict happens, we can still log an error and let the admin handle the error?

Hmm, right, so the problem is if Radarr already has a tag 1-magiclegend, but my user in Seerr is MagicLegend it would now try to find the tag 1-MagicLegend which doesn't exist, and throw the errors I mentioned a few comments ago. I actually have this exact problem with how my Radarr/Sonarr have their tags set up as a leftover remnant. My old pre-existing tags are called 1 - magiclegend but my user is MagicLegend. So the current code would try to check for 1 - MagicLegend, which doesn't exist. Simply changing to a lowercase comparison would silently solved this problem.

Since, like you also mentioned, both Radarr and Sonarr do not allow duplicate usernames I believe the simple fix would be to simply match tags based on lowercase. I don't think an administrator would need to do anything, since this is a simple fix on our side that does not introduce issues with possible conflicts.

An alternative solution would be an extra check before trying to create a new tag:

diff --git a/server/subscriber/MediaRequestSubscriber.ts b/server/subscriber/MediaRequestSubscriber.ts
index d8b636b6..5804522c 100644
--- a/server/subscriber/MediaRequestSubscriber.ts
+++ b/server/subscriber/MediaRequestSubscriber.ts
@@ -328,6 +328,29 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface<MediaRe
        if (radarrSettings.tagRequests) {
          const radarrTags = await radarr.getTags();
          const tagFormat =
            radarrSettings.tagRequestsFormat ??
            TagRequestsFormat.USERID_USERNAME;

          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 + '-')
          );
          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);
            userTag = radarrTags.find((v) => v.label === tagLabel);
          } else if (!userTag) {
            // old tags had space around the hyphen
            userTag = radarrTags.find((v) =>
              v.label.startsWith(entity.requestedBy.id + ' - ')
            );
          }
+          if (!userTag) {
+            // Radarr treats tag labels as case-insensitively unique, but our lookups
+            // above are case-sensitive. Fall back to a case-insensitive match so we
+            // don't attempt to create a tag that only differs by case, which Radarr
+            // will reject with a UNIQUE constraint error.
+            const caseInsensitiveMatch = radarrTags.find(
+              (v) => v.label.toLowerCase() === tagLabel.toLowerCase()
+            );
+            if (caseInsensitiveMatch) {
+              logger.warn(
+                `Requester's tag already exists in Radarr with different casing. Using existing tag to avoid a duplicate tag creation error`,
+                {
+                  label: 'Media Request',
+                  requestId: entity.id,
+                  mediaId: entity.media.id,
+                  userId: entity.requestedBy.id,
+                  expectedTag: tagLabel,
+                  existingTag: caseInsensitiveMatch.label,
+                }
+              );
+              userTag = caseInsensitiveMatch;
+            }
+          }
          if (!userTag) {
            logger.info(`Requester has no active tag. Creating new`, {
              label: 'Media Request',
              requestId: entity.id,
              mediaId: entity.media.id,
              userId: entity.requestedBy.id,
              newTag: tagLabel,
            });
            userTag = await radarr.createTag({ label: tagLabel });
          }
          if (userTag.id) {
            if (!tags?.find((v) => v === userTag?.id)) {
              tags?.push(userTag.id);
            }
          } else {
            logger.warn(`Requester has no tag and failed to add one`, {
              label: 'Media Request',
              requestId: entity.id,
              mediaId: entity.media.id,
              userId: entity.requestedBy.id,
              radarrServer: radarrSettings.hostname + ':' + radarrSettings.port,
            });
          }
        }

I'm not a fan of this solution, since I believe you can simply check for the lowercase names. The change-to-lowercase approach:

diff --git a/server/subscriber/MediaRequestSubscriber.ts b/server/subscriber/MediaRequestSubscriber.ts
index 5804522c..ac1f709e 100644
--- a/server/subscriber/MediaRequestSubscriber.ts
+++ b/server/subscriber/MediaRequestSubscriber.ts
@@ -313,44 +313,27 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface<MediaRe
        if (radarrSettings.tagRequests) {
          const radarrTags = await radarr.getTags();
          const tagFormat =
            radarrSettings.tagRequestsFormat ??
            TagRequestsFormat.USERID_USERNAME;

          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
+          // tag labels are compared case-insensitively, since Radarr enforces case-insensitive
+          // uniqueness on tag labels
           let userTag = radarrTags.find((v) =>
-            v.label.startsWith(entity.requestedBy.id + '-')
+            v.label.toLowerCase().startsWith(entity.requestedBy.id + '-')
           );
           if (tagFormat === TagRequestsFormat.USERID) {
             tagLabel = String(entity.requestedBy.id);
-            userTag = radarrTags.find((v) => v.label === tagLabel);
+            userTag = radarrTags.find(
+              (v) => v.label.toLowerCase() === tagLabel.toLowerCase()
+            );
           } else if (tagFormat === TagRequestsFormat.USERNAME) {
             tagLabel = sanitizeDisplayName(entity.requestedBy.displayName);
-            userTag = radarrTags.find((v) => v.label === tagLabel);
+            userTag = radarrTags.find(
+              (v) => v.label.toLowerCase() === tagLabel.toLowerCase()
+            );
           } else if (!userTag) {
             // old tags had space around the hyphen
             userTag = radarrTags.find((v) =>
-              v.label.startsWith(entity.requestedBy.id + ' - ')
+              v.label.toLowerCase().startsWith(entity.requestedBy.id + ' - ')
             );
           }
          if (!userTag) {
            logger.info(`Requester has no active tag. Creating new`, {
              label: 'Media Request',
              requestId: entity.id,
              mediaId: entity.media.id,
              userId: entity.requestedBy.id,
              newTag: tagLabel,
            });
            userTag = await radarr.createTag({ label: tagLabel });
          }
          if (userTag.id) {
            if (!tags?.find((v) => v === userTag?.id)) {
              tags?.push(userTag.id);
            }
          } else {
            logger.warn(`Requester has no tag and failed to add one`, {
              label: 'Media Request',
              requestId: entity.id,
              mediaId: entity.media.id,
              userId: entity.requestedBy.id,
              radarrServer: radarrSettings.hostname + ':' + radarrSettings.port,
            });
          }
        }

I agree that this might not be a problem that a user would run into often, but as I mentioned it's a problem that I personally ran into with how my Radarr/Sonarr was set up from the past. And given how simple the fix is I don't see a reason not to simply do this.

I still don't understand this argument. Checking the legacy tag first goes against the code-optimization tricks I've learned over the years, what am I missing in my thinking to want to do it this way? As you say yourself: Retrieving the old tags with spaces is just a fallback if the other migration failed.. Why should the fallback be the first thing the code considers?

Simply because this fallback is pointless if it's not run before the new behavior. We want to retrieve old that have not been migrated yet. If we create the new tag without checking for the old tag first, this fallback is useless.

What we could consider is removing this fallback, and rely only on the migration running at startup. But this migration is not bullet-proof, since it can fail because of a network issue or if the *arr is not available at this moment.

We want to retrieve old that have not been migrated yet. If we create the new tag without checking for the old tag first, this fallback is useless. That's correct, which is why the code doesn't create the new tag if an old-style tag was found. That's what the last else if (!userTag) is for. But I envision that if both new-style 1-magiclegend and old-style 1 - magiclegend somehow exist, it should prefer to use the new-style 1-magiclegend.

If it would continue to assign movies to the old tag, it would only increase the amount of manual work the administrator needs to do to migrate (because you have to manually unassign all tags from all movies before you can remove a tag). Another option would be to make sure the migration can be called manually from the settings page.

Note again that this case would only happen if both the old and new style tags would exist.

@fallenbagel wdyt?

I still miss this feature every time a romantic comedy for my parents comes up in my library because I can't easily separate their requests from mine in Radarr 😄

@github-actions

Copy link
Copy Markdown

This PR is stale because it has been open 30 days with no activity. Please address the feedback or provide an update to keep it open.

@github-actions github-actions Bot added the stale label Aug 22, 2026
@aussierk

Copy link
Copy Markdown
Contributor

I think I f this could be a migration function that upon changing your tag format, It asks if you wish to update the existing tags and pushes a modification to Radarr/Sonarr etc...

This way the check isn't run every single time but offers a per change opportunity to fix the tags.

@fallenbagel

Copy link
Copy Markdown
Member

I think I f this could be a migration function that upon changing your tag format, It asks if you wish to update the existing tags and pushes a modification to Radarr/Sonarr etc...

This way the check isn't run every single time but offers a per change opportunity to fix the tags.

Oh sort of like how immich does their media storage template migrations right?

@aussierk

Copy link
Copy Markdown
Contributor

I think I f this could be a migration function that upon changing your tag format, It asks if you wish to update the existing tags and pushes a modification to Radarr/Sonarr etc...
This way the check isn't run every single time but offers a per change opportunity to fix the tags.

Oh sort of like how immich does their media storage template migrations right?

Yeah, exactly.

@github-actions github-actions Bot removed the stale label Aug 28, 2026
@github-actions

ghost commented Sep 28, 2026

Copy link
Copy Markdown

This PR is stale because it has been open 30 days with no activity. Please address the feedback or provide an update to keep it open.

@github-actions github-actions Bot added the stale label Sep 28, 2026
@github-actions

ghost commented Oct 29, 2026

Copy link
Copy Markdown

This PR was closed because it has been stalled for 30 days with no activity. You can reopen it once you address the feedback or provide the requested changes.

@github-actions github-actions Bot closed this Oct 29, 2026
@0xSysR3ll 0xSysR3ll reopened this Oct 29, 2026
@github-actions github-actions Bot removed the stale label Oct 30, 2026

This branch has not been deployed

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

8 participants