Skip to content

feat(settings): add endpoint and UI for switching media server#2539

Draft
0xSysR3ll wants to merge 34 commits intodevelopfrom
0xsysr3ll/feat/media-server-migration
Draft

feat(settings): add endpoint and UI for switching media server#2539
0xSysR3ll wants to merge 34 commits intodevelopfrom
0xsysr3ll/feat/media-server-migration

Conversation

@0xSysR3ll
Copy link
Copy Markdown
Contributor

@0xSysR3ll 0xSysR3ll commented Feb 20, 2026

Description

Caution

Work in progress

This feature is still experimental. Do not use it on a production instance unless you have a verified backup and a tested rollback plan.
See Backups.

Also, don't use this image if you did not migrate to at least Seerr v3.1.0

This PR adds an in‑app way to switch a single Seerr instance between media servers (Plex <=> Jellyfin/Emby) without losing users, permissions, requests, or general settings.
The same database and application instance are reused; only the active media server and authentication method change.

The goal is to make media‑server migrations practical without needing a second Seerr instance or manual database edits. You (the owner) configure the new server, users link their new accounts, and then the instance is switched in place.

Note

This will initially ship as a preview so the flow can be tested and refined properly.

Migration flow (preview)

  1. Back up first

    • Follow the official guide: Backups.
  2. Switch to the preview image

    • Stop seerr.
    • Change the image tag to preview-media-server-migration
    • Start Seerr
  3. Configure your target media server

    • Plex => Jellyfin/Emby: configure Jellyfin/Emby in the Jellyfin settings tab.
    • Jellyfin/Emby => Plex: configure Plex in the Plex settings tab.

Warning

Jellyfin <=> Emby is not a supported migration path, at least not properly tested yet.

  1. Have users link their accounts

    • Users go to Profile => Linked accounts and link their account on the target media server.
    • (Optional) In Settings => Users, verify who has "Plex linked" or "Jellyfin/Emby linked".
  2. Switch media server in settings

    • As an admin, open Settings => General.
    • Use the "Switch media server" section to choose the target server and confirm.
  3. Restart and sign in again

    • After a successful switch, all users are logged out.
    • Restart Seerr, then have users sign in with the new account they linked.
  4. Verify your data

    • Confirm users, permissions, requests, and general settings look correct.
    • If anything went wrong, restore from your backup and report the issue.
  5. Go back to production image

    • Stop Seerr
    • Change the image tag back to latest or v3.1.0
    • Start Seerr

How Has This Been Tested?

  • Spun up a dev Seerr instance with Plex as media server ;
  • Imported some Plex users ;
  • Seeded the database (issues, requests, blocklist, etc) with a script to reflect a production server ;
  • Configured Jellyfin media server in Seerr settings ;
  • Linked the users to their corresponding Jellyfin account ;
  • Switched from Plex to Jellyfin ;

=> And vice-versa from Jellyfin to Plex.

Screenshots / Logs (if applicable)

Checklist:

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

Summary by CodeRabbit

  • New Features

    • Add API and admin Settings UI to switch the active media server (Plex, Jellyfin, Emby) with a confirmation modal, progress steps, toasts, and automatic reload.
  • Improvements

    • Allow linking/unlinking flows to work when non-primary server configs exist; clearer validation and “already linked” handling.
    • User list now shows linked-server status badges; settings UI shows a switch section for admins.
  • Localization

    • Added i18n keys for switching flow, warnings, buttons, and linked-account labels.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 20, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2fd3d988-b552-4725-a732-6e66cfd9dd51

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds an admin-controlled media-server switching feature: new POST API to switch servers, server-side migration logic updating Settings, Users, Media, Watchlists, and Sessions, new UI to initiate switches, relaxed account linking flows, and a stricter Media update guard.

Changes

Cohort / File(s) Summary
API Definition
seerr-api.yml
Added POST /settings/switch-media-server endpoint with `{ targetServerType: "jellyfin"
Server — Settings & Switching
server/routes/settings/index.ts
New admin route /switch-media-server: validates config/tokens, mutates Settings, Users, Media, Watchlist, Sessions; clears server-specific fields, restarts background jobs, and returns user-facing messages.
Server — Auth
server/routes/auth.ts
Plex POST: validate/store Plex token when Plex isn't main; Jellyfin/Emby POST: consolidate update logic and set userType based on current main server.
Server — User Linking
server/routes/user/usersettings.ts
Link/unlink flows for Plex/Jellyfin/Emby adjusted: use findOne, handle already-linked vs conflict, relax gating to allow linking when respective config exists, and resolve hostnames based on main vs linked config.
Server — Subscribers
server/subscriber/MediaSubscriber.ts
Tightened beforeUpdate/afterUpdate guards to require event.databaseEntity and event.entity; removed optional chaining in status reads.
UI — Settings Layout & Main
src/components/Settings/SettingsLayout.tsx, src/components/Settings/SettingsMain/index.tsx
Settings tabs computed from current mediaServerType; admin-only SwitchMediaServerSection inserted into settings UI.
UI — Switch Media Server
src/components/Settings/SwitchMediaServerSection.tsx
New admin component with modal workflow to select target, validate available targets, POST to API, show toasts, and reload on success.
UI — Plex Settings
src/components/Settings/SettingsPlex.tsx
Refactored to support main vs linked Plex flows, added Plex login button for linking, adjusted loading and form behavior, and token posting to auth endpoint.
UI — Linked Accounts / User List
src/components/UserProfile/.../UserLinkedAccountsSettings/index.tsx, src/components/UserList/index.tsx
Linked account discovery and visibility updated to reflect presence of linkage fields regardless of userType; user list shows additional linked badges and a switch-media-server tip.
i18n / Misc / Small
src/i18n/locale/en.json, server/middleware/deprecation.ts
Added translation keys for switch flow and linking; minor deprecation log message character change.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant UI as Settings UI
    participant API as Settings API
    participant DB as Database
    participant BG as Background Jobs

    User->>UI: Select target server & confirm
    UI->>API: POST /settings/switch-media-server { targetServerType }

    API->>API: Validate current settings & tokens
    API->>DB: Begin transaction / update Settings

    rect rgba(100,150,200,0.5)
        API->>DB: Update User records (clear/set server fields)
        API->>DB: Reset Media records (clear server-specific IDs)
        API->>DB: Reset Watchlist entries
        API->>DB: Purge Sessions
    end

    API->>DB: Commit transaction
    API->>BG: Restart scheduled background jobs
    BG->>BG: Reinitialize with new server config

    API-->>UI: 200 Success
    UI->>User: Show success toast and reload
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • fallenbagel
  • gauthier-th

Poem

🐰 I hopped and flipped the server with a nudge so bold,
Cleared keys, logged out folks, and watched the jobs unfold.
Users relink, badges shine, the modal gave the cue,
The rabbit clicked confirm — then the dashboard felt brand new. 🥕

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning All changes are directly related to media server switching functionality. The deprecation.ts change (arrow character replacement) is a minor unrelated fix that falls outside the core feature scope. Remove the unrelated deprecation.ts change (arrow character replacement) or move it to a separate PR to keep this PR focused on media server switching functionality.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(settings): add endpoint and UI for switching media server' directly and clearly describes the primary changes: adding an endpoint and UI component for media server switching.
Linked Issues check ✅ Passed The PR implements a comprehensive media server migration mechanism that directly addresses issue #2522 by enabling in-app conversion between Plex and Jellyfin/Emby with user account linking, configuration persistence, and an admin control for performing the switch.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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 and usage tips.

@0xSysR3ll
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 20, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Don't mind the big refactor on this one, I had some UI issues that generated a lot of changes (it's mainly spaces)

Copy link
Copy Markdown

@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: 6

Caution

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

⚠️ Outside diff range comments (1)
server/routes/settings/index.ts (1)

175-219: ⚠️ Potential issue | 🟡 Minor

Handle missing Plex token explicitly.

If the admin token is absent (e.g., post-switch), this returns undefined with 200. A clear 400 response is safer.

💡 Suggested fix
-    const authToken = admin.plexToken ?? null;
-    const plexTvClient = authToken ? new PlexTvAPI(authToken) : null;
+    const authToken = admin.plexToken ?? null;
+    if (!authToken) {
+      return next({
+        status: 400,
+        message: 'Plex token missing. Sign in with Plex first.',
+      });
+    }
+    const plexTvClient = new PlexTvAPI(authToken);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/routes/settings/index.ts` around lines 175 - 219, The route currently
proceeds when the admin Plex token is missing and later returns undefined;
before creating PlexTvAPI/calling plexTvClient.getDevices(), check
admin.plexToken (authToken) and if null/undefined immediately return a 400 Bad
Request response (with a clear message like "Plex token missing") instead of
continuing; update the logic around getSettings / userRepository.findOneOrFail /
PlexTvAPI / plexTvClient to short-circuit on missing authToken so no calls to
plexTvClient.getDevices() or PlexAPI are attempted.
🧹 Nitpick comments (1)
seerr-api.yml (1)

2293-2320: Document the optional target server type in the request body.

Clients won’t know how to request Emby vs. Jellyfin without a schema. Consider adding an optional targetServerType field.

📝 Suggested OpenAPI addition
   /settings/switch-media-server:
     post:
       summary: Switch media server
       tags:
         - settings
+      requestBody:
+        required: false
+        content:
+          application/json:
+            schema:
+              type: object
+              properties:
+                targetServerType:
+                  type: string
+                  enum: [jellyfin, emby]
+                  description: Target server type; defaults to jellyfin when omitted.
       responses:
         '200':
           description: Media server cleared
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@seerr-api.yml` around lines 2293 - 2320, The POST
/settings/switch-media-server operation lacks a requestBody schema describing
how to specify which server type to switch to; add a requestBody (content:
application/json) with an optional property named targetServerType (string) and
constrain it to allowed values (e.g., enum: ["emby","jellyfin"]) so clients know
how to request Emby vs Jellyfin; update the operation under the path
"/settings/switch-media-server" to include this requestBody and mark the
property as optional and documented in the response examples.
🤖 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/routes/auth.ts`:
- Around line 61-92: The current block lets any authenticated user overwrite the
admin Plex token; update the authorization to allow only admins to perform this
action by checking the authenticated user's admin role (e.g., req.user.isAdmin
or req.user.role === 'admin') before proceeding with PlexTvAPI validation and
saving to userRepository; if the user is not an admin, return a 403 error (do
not change the existing admin user retrieval logic that uses findOneOrFail for
id 1 and the PlexTvAPI validation flow).

In `@server/routes/settings/index.ts`:
- Around line 453-569: The /switch-media-server handler
(settingsRoutes.post('/switch-media-server', ...)) is unprotected; add an
admin-only guard at the top of the handler (or attach an existing admin
middleware) so only admins can call it: verify req.user exists and has admin
rights (e.g., check req.user.userType === UserType.ADMIN or an isAdmin flag on
the User entity) and return 403 if not authorized before any state changes
(before getSettings(), repository updates, settings.save(), startJobs(), etc.);
reference User and UserType used in the route for the check or reuse your app's
ensureAdmin middleware if available.

In `@server/routes/user/usersettings.ts`:
- Around line 419-429: The unlink (Jellyfin delete) route currently blocks
unlinking when Jellyfin/Emby isn't the main server; update its guard to mirror
the linking check by using the same logic as isMainJellyfin and
jellyfinForLinking: allow unlink if either settings.main.mediaServerType ===
MediaServerType.JELLYFIN/EMBY (isMainJellyfin) or settings.jellyfin?.ip is
present; replace the strict isMainJellyfin-only check in the delete handler with
the combined condition (!isMainJellyfin && !jellyfinForLinking?.ip) so users who
linked against a configured non-main Jellyfin can unlink.

In `@src/components/Settings/SettingsPlex.tsx`:
- Around line 792-801: The Formik form for Tautulli (the Formik component with
initialValues built from dataTautulli and using TautulliSettingsSchema) can
mount before dataTautulli is available and won't pick up updates; enable
reinitialization by adding enableReinitialize to the Formik props (or
conditionally render Formik only when dataTautulli is defined) so the form's
initialValues update when dataTautulli arrives and avoid blank
defaults/accidental overwrites.

In `@src/components/Settings/SwitchMediaServerSection.tsx`:
- Around line 107-113: In the catch block inside SwitchMediaServerSection (the
try/catch that builds the toast message), broaden the Axios error extraction to
check response.data.message as well as response.data.error and fallback to
err.message before using the generic intl.formatMessage; update the conditional
that sets message (currently using axios.isAxiosError(err) &&
err.response?.data?.error) to instead test axios.isAxiosError(err) and then
prefer String(err.response?.data?.error ?? err.response?.data?.message ??
err.message) so you capture endpoints that return message or a plain Error
message while preserving the existing generic fallback.
- Around line 95-106: The success toast message is hard-coded; replace it with a
localized string by calling the project's i18n translator instead of the literal
text. In the onClick handler inside SwitchMediaServerSection (where addToast is
called after axios.post), import or use the existing translation function (e.g.,
useTranslation's t or i18n.t) and pass a translation key like
t('settings.mediaServer.cleared') (with an appropriate entry added to your
locale files) to addToast instead of the hard-coded "Media server cleared. You
may need to restart."; keep the same appearance options and reload logic.

---

Outside diff comments:
In `@server/routes/settings/index.ts`:
- Around line 175-219: The route currently proceeds when the admin Plex token is
missing and later returns undefined; before creating PlexTvAPI/calling
plexTvClient.getDevices(), check admin.plexToken (authToken) and if
null/undefined immediately return a 400 Bad Request response (with a clear
message like "Plex token missing") instead of continuing; update the logic
around getSettings / userRepository.findOneOrFail / PlexTvAPI / plexTvClient to
short-circuit on missing authToken so no calls to plexTvClient.getDevices() or
PlexAPI are attempted.

---

Nitpick comments:
In `@seerr-api.yml`:
- Around line 2293-2320: The POST /settings/switch-media-server operation lacks
a requestBody schema describing how to specify which server type to switch to;
add a requestBody (content: application/json) with an optional property named
targetServerType (string) and constrain it to allowed values (e.g., enum:
["emby","jellyfin"]) so clients know how to request Emby vs Jellyfin; update the
operation under the path "/settings/switch-media-server" to include this
requestBody and mark the property as optional and documented in the response
examples.

Comment thread server/routes/auth.ts
Comment thread server/routes/settings/index.ts Outdated
Comment thread server/routes/user/usersettings.ts
Comment thread src/components/Settings/SettingsPlex.tsx
Comment thread src/components/Settings/SwitchMediaServerSection.tsx Outdated
Comment thread src/components/Settings/SwitchMediaServerSection.tsx Outdated
@0xSysR3ll 0xSysR3ll force-pushed the 0xsysr3ll/feat/media-server-migration branch from 41a7f8b to 3786a27 Compare February 20, 2026 22:42
@github-actions github-actions Bot added the i18n-out-of-sync User forgot to run pnpm i18n:extract label Feb 20, 2026
@github-actions
Copy link
Copy Markdown

The i18n check failed because translation messages are out of sync.

This usually happens when you've added or modified translation strings in your code but haven't updated the translation file.

Please run pnpm i18n:extract and commit the changes.

@github-actions github-actions Bot removed the i18n-out-of-sync User forgot to run pnpm i18n:extract label Feb 20, 2026
@0xSysR3ll
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 21, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@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 (2)
server/subscriber/MediaSubscriber.ts (1)

155-158: Guard logic is correct; consider removing redundant optional chaining.

The guard now ensures both event.entity and event.databaseEntity are defined. However, lines 178 and 191 still use event.databaseEntity?.status and event.databaseEntity?.status4k — the optional chaining is now redundant and could be simplified for consistency.

♻️ Optional cleanup
     if (
-      (event.entity.status !== event.databaseEntity?.status ||
+      (event.entity.status !== event.databaseEntity.status ||
         (event.entity.mediaType === MediaType.TV &&
           seasonStatusCheck(false))) &&
       validStatuses.includes(event.entity.status)
     if (
-      (event.entity.status4k !== event.databaseEntity?.status4k ||
+      (event.entity.status4k !== event.databaseEntity.status4k ||
         (event.entity.mediaType === MediaType.TV && seasonStatusCheck(true))) &&
       validStatuses.includes(event.entity.status4k)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/subscriber/MediaSubscriber.ts` around lines 155 - 158, The guard in
MediaSubscriber.afterUpdate already ensures event.entity and
event.databaseEntity are non-null, so remove the redundant optional chaining:
replace occurrences of event.databaseEntity?.status and
event.databaseEntity?.status4k with direct access event.databaseEntity.status
and event.databaseEntity.status4k (e.g., in the conditional checks currently
using ?. at lines referencing those properties) while keeping the existing
early-return guard intact.
server/routes/settings/index.ts (1)

479-492: Consider: User update queries always match all users.

The WHERE clause user.id >= :zero with zero: 0 will always match all users since IDs are positive integers. While this works correctly, it's semantically unclear.

Consider simplifying to make the intent explicit:

♻️ Suggested clarification
         await userRepository
           .createQueryBuilder()
           .update(User)
           .set({ plexId: null, plexUsername: null, plexToken: null })
-          .where('user.id >= :zero', { zero: 0 })
+          .execute();  // Updates all users

Or if you need a WHERE clause for safety:

-          .where('user.id >= :zero', { zero: 0 })
+          .where('user.id IS NOT NULL')  // All users
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/routes/settings/index.ts` around lines 479 - 492, The first update
query uses a redundant WHERE ('user.id >= :zero', { zero: 0 }) that effectively
matches all users; remove that semantic noise by calling the same
userRepository.createQueryBuilder().update(User).set({ plexId: null,
plexUsername: null, plexToken: null }).execute() so the intent to update all
rows is explicit and clear (keep the second query using
.where('user.jellyfinUserId IS NOT NULL') as-is).
🤖 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/routes/settings/index.ts`:
- Around line 500-506: The watchlist clearing is asymmetric and uses
inconsistent null handling: update the Watchlist clearing logic that uses
watchlistRepository.createQueryBuilder().update(Watchlist) so that
Watchlist.ratingKey is cleared whenever switching away from Plex (add the
corresponding branch or call in the switch-to-Plex/from-Plex flows), and make
the clearing consistent with other media fields by setting ratingKey to null
instead of ''. Replace .set({ ratingKey: '' }) with .set({ ratingKey: null })
and change the predicate to .where("watchlist.ratingKey IS NOT NULL") (or
equivalent ORM null check), and ensure the same null convention is applied
wherever media fields are cleared.

---

Nitpick comments:
In `@server/routes/settings/index.ts`:
- Around line 479-492: The first update query uses a redundant WHERE ('user.id
>= :zero', { zero: 0 }) that effectively matches all users; remove that semantic
noise by calling the same userRepository.createQueryBuilder().update(User).set({
plexId: null, plexUsername: null, plexToken: null }).execute() so the intent to
update all rows is explicit and clear (keep the second query using
.where('user.jellyfinUserId IS NOT NULL') as-is).

In `@server/subscriber/MediaSubscriber.ts`:
- Around line 155-158: The guard in MediaSubscriber.afterUpdate already ensures
event.entity and event.databaseEntity are non-null, so remove the redundant
optional chaining: replace occurrences of event.databaseEntity?.status and
event.databaseEntity?.status4k with direct access event.databaseEntity.status
and event.databaseEntity.status4k (e.g., in the conditional checks currently
using ?. at lines referencing those properties) while keeping the existing
early-return guard intact.

Comment thread server/routes/settings/index.ts Outdated
@0xSysR3ll 0xSysR3ll added the preview PRs deployed for testing with tag `:preview-prxx` label Feb 22, 2026
@0xSysR3ll 0xSysR3ll force-pushed the 0xsysr3ll/feat/media-server-migration branch 2 times, most recently from 99ba4fb to ce959d6 Compare February 27, 2026 20:24
@0xSysR3ll 0xSysR3ll force-pushed the 0xsysr3ll/feat/media-server-migration branch from ce959d6 to bec2302 Compare March 9, 2026 19:32
@0xSysR3ll
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 9, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@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: 6

Caution

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

⚠️ Outside diff range comments (2)
server/routes/settings/index.ts (1)

126-160: ⚠️ Potential issue | 🟠 Major

Validate against a temporary Plex config first.

Object.assign(settings.plex, plexBody) mutates live process state before getStatus() succeeds. If the connection test fails, the request returns an error but the app keeps serving the unsaved, unverified Plex host/port until something overwrites it. Mirror the Jellyfin path here and only copy into settings.plex after validation passes.

Proposed fix
   const settings = getSettings();
   const body = req.body as Record<string, unknown>;
   const { authToken: bodyToken, ...plexBody } = body;
   try {
     const admin = await userRepository.findOneOrFail({
       select: { id: true, plexToken: true },
       where: { id: 1 },
     });

-    Object.assign(settings.plex, plexBody);
+    const tempPlexSettings = { ...settings.plex, ...plexBody };

     const token =
       settings.main.mediaServerType !== MediaServerType.PLEX &&
       typeof bodyToken === 'string'
         ? bodyToken
         : admin.plexToken;
@@
-    const plexClient = new PlexAPI({ plexToken: token });
+    const plexClient = new PlexAPI({
+      plexToken: token,
+      plexSettings: tempPlexSettings,
+    });

     const result = await plexClient.getStatus();
@@
-    settings.plex.machineId = result.MediaContainer.machineIdentifier;
-    settings.plex.name = result.MediaContainer.friendlyName;
+    Object.assign(settings.plex, tempPlexSettings, {
+      machineId: result.MediaContainer.machineIdentifier,
+      name: result.MediaContainer.friendlyName,
+    });

     await settings.save();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/routes/settings/index.ts` around lines 126 - 160, The current code
mutates live settings via Object.assign(settings.plex, plexBody) before
validating the Plex connection; instead, create a temporary plex config (e.g.,
const tempPlex = { ...settings.plex, ...plexBody }) and use that when
constructing PlexAPI and calling plexClient.getStatus(); only after getStatus()
confirms MediaContainer.machineIdentifier should you assign the validated values
into settings.plex and call settings.save(); ensure the fallback token logic
still uses bodyToken/admin.plexToken when building PlexAPI so the validation
uses the intended credentials.
src/components/UserProfile/UserSettings/UserLinkedAccountsSettings/index.tsx (1)

78-90: ⚠️ Potential issue | 🟠 Major

Don't hide non-main Jellyfin links behind userType.

When Jellyfin/Emby is linked while another server is still primary, the server-side link flow can leave user.userType unchanged. A local user that links here will therefore drop out of accounts, and the dropdown will still offer the link action instead of showing the linked account/unlink control. Please render secondary Jellyfin links from the stored linked fields, not just from userType.

Also applies to: 136-141

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

In `@src/components/UserProfile/UserSettings/UserLinkedAccountsSettings/index.tsx`
around lines 78 - 90, The code currently gates adding Emby/Jellyfin to accounts
on user.userType, causing linked secondary servers to be hidden; update the
logic in the component that builds the accounts array so it relies on the stored
linked fields (e.g., user.jellyfinUsername and user.embyUsername) rather than
user.userType: always push a LinkedAccountType.Jellyfin entry when
user.jellyfinUsername exists, and push a LinkedAccountType.Emby entry when
user.embyUsername (or the correct Emby username field) exists, removing the
user.userType checks around these pushes and ensuring you don't push duplicate
entries for the same username.
🤖 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/routes/auth.ts`:
- Around line 73-89: The handler currently overwrites the canonical Plex account
by always writing the validated Plex identity to the row found with
userRepository.findOneOrFail({ where: { id: 1 } }) (variables PlexTvAPI,
plextv.getUser, admin), allowing any admin to replace the system-wide Plex
token; change the guard so only the canonical owner can update that row: verify
req.user.id === 1 (or a dedicated owner flag on req.user) before assigning
admin.plexToken/plexId/plexUsername and saving via userRepository.save(admin);
alternatively, persist the validated token to the calling user's record instead
of id=1, or add a stable "isPrimary" flag on users and restrict writes to the
user where isPrimary === true and req.user is that same user. Ensure
req.user.hasPermission(Permission.ADMIN) remains but add the identity check
(req.user.id === 1 or req.user.isPrimary) before mutating the canonical account.

In `@server/routes/settings/index.ts`:
- Around line 561-572: The code calls setImmediate(() => startJobs()) after
clearing sessions which leads to duplicate schedulers because startJobs()
appends into scheduledJobs without clearing or cancelling existing jobs; update
the logic in the settings route (the branch that calls startJobs()) to first
cancel/clear existing scheduled jobs (e.g., iterate scheduledJobs and call their
cancel/clear method then empty the scheduledJobs array) or remove the
setImmediate call and defer starting jobs until process boot; apply the same fix
to the other occurrence around the startJobs() call at the 641-650 region to
ensure no duplicate schedulers are created.
- Around line 531-565: Wrap all DB mutations and the settings persistence into a
single database transaction so the media-server switch is atomic: perform the
repository updates currently using getRepository(User), getRepository(Media),
getRepository(Watchlist), the Session delete, and the settings.save() call
inside a single transactional callback (e.g., getManager().transaction or
getConnection().transaction) and only update the in-memory
settings.main.mediaServerType and settings.plex (and any other in-memory state)
after the transaction successfully commits; if the transaction fails, roll back
and do not modify in-memory values. Ensure the same change is applied for the
other branch mentioned (lines 603-645) so both switch paths use the transaction.

In `@server/routes/user/usersettings.ts`:
- Around line 339-345: The unlink handlers currently set user.userType =
UserType.LOCAL unconditionally, which downgrades accounts even when removing a
non-active (auxiliary) provider; change the logic to only set user.userType =
UserType.LOCAL when the provider being unlinked is the active auth provider
(compare settings.main.mediaServerType / MediaServerType.PLEX and the provider
being removed), otherwise leave user.userType unchanged and only remove the
provider-specific fields (e.g., plex fields checked by plexConfigured and
similar Jellyfin/Emby checks); update both unlink code paths (the Plex handler
around plexConfigured and the other handler referenced at lines ~508-516) to
perform this conditional fallback.

In `@src/components/Settings/SettingsPlex.tsx`:
- Around line 425-468: The onSubmit handler currently calls syncLibraries()
fire-and-forget which can lead to unhandled rejections and incorrect toasts;
modify the onSubmit block to await syncLibraries() so the save success toast
only appears after sync completion, or change the syncLibraries implementation
to ensure it always resets state by wrapping its body in try/finally
(setIsSyncing(true) before, finally setIsSyncing(false)) and await internal
axios.get('/api/v1/settings/plex/library') and revalidate() so errors are
handled and isSyncing is cleared.

In `@src/components/UserList/index.tsx`:
- Around line 731-745: Add plexId?: number | null and jellyfinUserId?: string |
null to the shared User interface in useUser (and adjust User.filteredFields /
User.filterMany() behavior if these fields are intentionally admin-only), then
remove the unsafe ad-hoc casts in the UserList component that check (user as {
plexId?: number | null }).plexId and (user as { jellyfinUserId?: string | null
}).jellyfinUserId; if these fields are only present for users with MANAGE_USERS
permission, document that constraint in a comment near the User interface and
ensure filteredFields/filterMany() consistently strips them for non-admins so
the badges render only when the field is actually present.

---

Outside diff comments:
In `@server/routes/settings/index.ts`:
- Around line 126-160: The current code mutates live settings via
Object.assign(settings.plex, plexBody) before validating the Plex connection;
instead, create a temporary plex config (e.g., const tempPlex = {
...settings.plex, ...plexBody }) and use that when constructing PlexAPI and
calling plexClient.getStatus(); only after getStatus() confirms
MediaContainer.machineIdentifier should you assign the validated values into
settings.plex and call settings.save(); ensure the fallback token logic still
uses bodyToken/admin.plexToken when building PlexAPI so the validation uses the
intended credentials.

In
`@src/components/UserProfile/UserSettings/UserLinkedAccountsSettings/index.tsx`:
- Around line 78-90: The code currently gates adding Emby/Jellyfin to accounts
on user.userType, causing linked secondary servers to be hidden; update the
logic in the component that builds the accounts array so it relies on the stored
linked fields (e.g., user.jellyfinUsername and user.embyUsername) rather than
user.userType: always push a LinkedAccountType.Jellyfin entry when
user.jellyfinUsername exists, and push a LinkedAccountType.Emby entry when
user.embyUsername (or the correct Emby username field) exists, removing the
user.userType checks around these pushes and ensuring you don't push duplicate
entries for the same username.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2f42b3ff-f404-4821-9ebb-ac6e2bb3d247

📥 Commits

Reviewing files that changed from the base of the PR and between ecfa295 and 1e872e5.

📒 Files selected for processing (13)
  • seerr-api.yml
  • server/middleware/deprecation.ts
  • server/routes/auth.ts
  • server/routes/settings/index.ts
  • server/routes/user/usersettings.ts
  • server/subscriber/MediaSubscriber.ts
  • src/components/Settings/SettingsLayout.tsx
  • src/components/Settings/SettingsMain/index.tsx
  • src/components/Settings/SettingsPlex.tsx
  • src/components/Settings/SwitchMediaServerSection.tsx
  • src/components/UserList/index.tsx
  • src/components/UserProfile/UserSettings/UserLinkedAccountsSettings/index.tsx
  • src/i18n/locale/en.json
✅ Files skipped from review due to trivial changes (1)
  • server/middleware/deprecation.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/components/Settings/SettingsLayout.tsx
  • seerr-api.yml

Comment thread server/routes/auth.ts
Comment thread server/routes/settings/index.ts
Comment thread server/routes/settings/index.ts
Comment thread server/routes/user/usersettings.ts
Comment thread src/components/Settings/SettingsPlex.tsx
Comment thread src/components/UserList/index.tsx
@github-actions github-actions Bot added the merge conflict Cannot merge due to merge conflicts label Mar 16, 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.

@0xSysR3ll 0xSysR3ll force-pushed the 0xsysr3ll/feat/media-server-migration branch from 1e872e5 to 5844680 Compare March 30, 2026 19:56
@github-actions github-actions Bot added i18n-out-of-sync User forgot to run pnpm i18n:extract and removed merge conflict Cannot merge due to merge conflicts labels Mar 30, 2026
@github-actions
Copy link
Copy Markdown

The i18n check failed because translation messages are out of sync.

This usually happens when you've added or modified translation strings in your code but haven't updated the translation file.

Please run pnpm i18n:extract and commit the changes.

@github-actions github-actions Bot removed the i18n-out-of-sync User forgot to run pnpm i18n:extract label Mar 30, 2026
@0xSysR3ll
Copy link
Copy Markdown
Contributor Author

@coderabbitai continue

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 30, 2026

@0xSysR3ll Sure! I'll continue the review of your PR right away.


🧠 Learnings used
Learnt from: michaelhthomas
Repo: seerr-team/seerr PR: 2715
File: src/components/UserProfile/UserSettings/UserLinkedAccountsSettings/index.tsx:248-250
Timestamp: 2026-03-26T16:06:30.667Z
Learning: Repo seerr-team/seerr — In server/routes/user/usersettings.ts, the DELETE endpoints for media servers (`/linked-accounts/plex` and `/linked-accounts/jellyfin`) require the user to have a local email and password; having multiple linked accounts alone is not sufficient. Client UI should gate media-server unlink on `hasPassword` (and primary-admin restriction) rather than `accounts.length > 1`.
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@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: 6

♻️ Duplicate comments (1)
src/components/Settings/SettingsPlex.tsx (1)

349-351: ⚠️ Potential issue | 🟠 Major

Guard Tautulli form render in non-main Plex mode to prevent accidental config wipe.

Line 349 no longer waits for Tautulli data when Plex is non-main, but Line 785 still renders the Tautulli form. This can mount with empty initial values and a valid submit path, so an early save can clear existing Tautulli settings.

Suggested fix
-  if (!isMainPlex && !data && !error) {
+  if (!isMainPlex && !data && !error) {
     return <LoadingSpinner />;
   }

@@
-        {!onComplete && (
+        {!onComplete && isMainPlex && (
           <>
             <div className="mb-6 mt-10">
               <h3 className="heading">
                 {intl.formatMessage(messages.tautulliSettings)}

Also applies to: 785-806

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

In `@src/components/Settings/SettingsPlex.tsx` around lines 349 - 351, The
Tautulli settings form can mount with empty initial values when Plex is non-main
because its render path isn’t guarded; update the Tautulli form render block to
require the same readiness check used above (respect isMainPlex and ensure data
or error is resolved) — i.e., only render the Tautulli form component when
isMainPlex is true or when data is loaded (or an error is present), otherwise
return the LoadingSpinner/skip render; adjust the conditional around the
Tautulli form render (the block that renders the Tautulli form component) to
mirror the earlier guard using isMainPlex, data, and error so an early submit
cannot wipe existing settings.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@seerr-api.yml`:
- Around line 2298-2307: The OpenAPI fragment is missing the required markers:
mark the requestBody as required (add requestBody: required: true) and add a
schema-level required array listing "targetServerType" (schema: required:
["targetServerType"]) so the request body and the property targetServerType are
enforced; update the requestBody and the schema surrounding targetServerType
accordingly.

In `@server/routes/settings/index.ts`:
- Around line 521-525: The route currently allows switching to 'jellyfin' or
'emby' even when the active server type is the opposite, but later clears the
shared Jellyfin/Emby linkage fields (see the code that clears user linkage
fields around the block handling lines 635-644); change the initial validation
around the target variable to return 400 if attempting a Jellyfin↔Emby swap
(i.e., if target === 'jellyfin' and currentServerType === 'emby' or vice versa)
so those transitions are rejected up front, and mirror the same restriction in
the frontend SwitchMediaServerSection component to prevent the UI from offering
that swap. Include references to the target variable, the current server type
check, and the user-linkage-clearing code so reviewers can locate the related
logic.
- Around line 527-539: The current guard in the settings route lets switching to
Plex succeed if admin?.plexToken exists even when settings.plex has no host
details; update the plexConfigured logic in the block that checks if (target ===
'plex') to require actual saved host info (settings.plex?.name or
settings.plex?.ip) and remove the admin?.plexToken fallback so plexConfigured
only becomes true when settings.plex host details are present, preserving the
existing 400 response when not configured.

In `@src/components/Settings/SettingsLayout.tsx`:
- Around line 33-46: In the MediaServerType.PLEX branch, replace the hardcoded
'Jellyfin' label in the intl.formatMessage call (the one building
messages.menuJellyfinSettings for the '/settings/jellyfin' entry) with the
configured secondary server name from
settings.currentSettings.primaryMediaServer (use a safe fallback like 'Jellyfin'
if that value is missing); update the code that constructs that menu item so it
pulls settings.currentSettings.primaryMediaServer and passes it into
intl.formatMessage instead of the literal string.

In `@src/components/Settings/SwitchMediaServerSection.tsx`:
- Around line 27-56: The step strings use placeholders {profile} and
{linkedAccounts} but the actual inserted labels are still hard-coded English;
add localized message entries (e.g., profileLabel and linkedAccountsLabel) to
the messages object and replace any hard-coded insertions by passing those
localized values when rendering the steps (e.g., values: { profile:
intl.formatMessage(messages.profileLabel), linkedAccounts:
intl.formatMessage(messages.linkedAccountsLabel) }); apply the same change for
the other affected keys referenced in this diff (switchMediaServerStep2Plex,
switchMediaServerStep2JellyfinEmby and the entries noted around lines 173-177).

In `@src/components/UserList/index.tsx`:
- Around line 96-99: The switch hint copy in switchMediaServerTip is misleading
— change it to state that only users linked to the target server can sign back
in (not either badge); update the message to something like "Only users linked
to the target media server (e.g., 'Plex linked' when switching to Plex) can sign
in after you switch media server in {generalSettings}." Locate
switchMediaServerTip and the related badge labels linkedToPlex and
linkedToJellyfinEmby and update the copy accordingly, and apply the same wording
fix to the other switch-hint occurrences in the file.

---

Duplicate comments:
In `@src/components/Settings/SettingsPlex.tsx`:
- Around line 349-351: The Tautulli settings form can mount with empty initial
values when Plex is non-main because its render path isn’t guarded; update the
Tautulli form render block to require the same readiness check used above
(respect isMainPlex and ensure data or error is resolved) — i.e., only render
the Tautulli form component when isMainPlex is true or when data is loaded (or
an error is present), otherwise return the LoadingSpinner/skip render; adjust
the conditional around the Tautulli form render (the block that renders the
Tautulli form component) to mirror the earlier guard using isMainPlex, data, and
error so an early submit cannot wipe existing settings.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8e666292-6966-43c2-98fa-8f0e5ff2fe14

📥 Commits

Reviewing files that changed from the base of the PR and between 1e872e5 and 1b2d811.

📒 Files selected for processing (13)
  • seerr-api.yml
  • server/middleware/deprecation.ts
  • server/routes/auth.ts
  • server/routes/settings/index.ts
  • server/routes/user/usersettings.ts
  • server/subscriber/MediaSubscriber.ts
  • src/components/Settings/SettingsLayout.tsx
  • src/components/Settings/SettingsMain/index.tsx
  • src/components/Settings/SettingsPlex.tsx
  • src/components/Settings/SwitchMediaServerSection.tsx
  • src/components/UserList/index.tsx
  • src/components/UserProfile/UserSettings/UserLinkedAccountsSettings/index.tsx
  • src/i18n/locale/en.json
✅ Files skipped from review due to trivial changes (1)
  • src/i18n/locale/en.json
🚧 Files skipped from review as they are similar to previous changes (6)
  • server/middleware/deprecation.ts
  • server/subscriber/MediaSubscriber.ts
  • server/routes/auth.ts
  • src/components/UserProfile/UserSettings/UserLinkedAccountsSettings/index.tsx
  • src/components/Settings/SettingsMain/index.tsx
  • server/routes/user/usersettings.ts

Comment thread seerr-api.yml
Comment thread server/routes/settings/index.ts
Comment thread server/routes/settings/index.ts
Comment thread src/components/Settings/SettingsLayout.tsx
Comment thread src/components/Settings/SwitchMediaServerSection.tsx
Comment thread src/components/UserList/index.tsx Outdated
@0xSysR3ll 0xSysR3ll force-pushed the 0xsysr3ll/feat/media-server-migration branch from 963b23b to cb56135 Compare April 25, 2026 07:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

preview PRs deployed for testing with tag `:preview-prxx`

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Convert to/from Plex/Jellyfin

1 participant