Skip to content

feat(plex): add Plex watchlist management functionality#2179

Closed
gwlsn wants to merge 5 commits intoseerr-team:developfrom
gwlsn:feat-plex-watchlist
Closed

feat(plex): add Plex watchlist management functionality#2179
gwlsn wants to merge 5 commits intoseerr-team:developfrom
gwlsn:feat-plex-watchlist

Conversation

@gwlsn
Copy link
Copy Markdown

@gwlsn gwlsn commented Nov 28, 2025

Redacted

@gwlsn gwlsn requested a review from a team as a code owner November 28, 2025 05:16
@gwlsn gwlsn force-pushed the feat-plex-watchlist branch from 4cb9369 to a912f56 Compare November 29, 2025 21:14
@buzdriver
Copy link
Copy Markdown

Hi,
Not sure what happend here but I got like 50 re-requests of items already available from people that requested already requested material.
/BR

@gwlsn
Copy link
Copy Markdown
Author

gwlsn commented Dec 1, 2025

Hi, Not sure what happend here but I got like 50 re-requests of items already available from people that requested already requested material. /BR

Hi,

Thanks reporting this! This PR only adds the ability to manually add/remove items from your Plex watchlist via buttons on the movie/TV details pages. It adds a couple new endpoints but it doesn't interact with the request system, database, or the existing Plex watchlist sync job, so the re-requests you're seeing are likely unrelated to these changes.

@gwlsn
Copy link
Copy Markdown
Author

gwlsn commented Dec 11, 2025

Hey, just checking if this is on your radar or if there’s anything I should change.

Cheers! Looking forward to the release.

@fallenbagel
Copy link
Copy Markdown
Collaborator

Hey, just checking if this is on your radar or if there’s anything I should change.

Cheers! Looking forward to the release.

Seerr is on a feature freeze. Only bugfixes. Until first release.

@gwlsn
Copy link
Copy Markdown
Author

gwlsn commented Dec 11, 2025

Hey, just checking if this is on your radar or if there’s anything I should change.

Cheers! Looking forward to the release.

Seerr is on a feature freeze. Only bugfixes. Until first release.

Totally understand, thanks for the heads up. Happy to rebase once the freeze lifts if needed. Good luck with the release!

@gwlsn gwlsn closed this Feb 7, 2026
@0xSysR3ll
Copy link
Copy Markdown
Contributor

@gwlsn Why did you close this ?

@gwlsn
Copy link
Copy Markdown
Author

gwlsn commented Feb 7, 2026

@gwlsn Why did you close this ?

Sorry, I closed this by mistake. Would you mind reopening it?

@M0NsTeRRR M0NsTeRRR reopened this Feb 7, 2026
Comment thread server/api/plextv.ts
Comment on lines +446 to +462
const response = await this.axios.get<DiscoverSearchResponse>(
'/library/search',
{
baseURL: 'https://discover.provider.plex.tv',
params: {
query,
searchTypes: type === 'movie' ? 'movies' : 'tv',
limit: 30,
searchProviders: 'discover',
includeMetadata: 1,
includeGuids: 1,
},
headers: {
Accept: 'application/json',
},
}
);

Check failure

Code scanning / CodeQL

Server-side request forgery

The [host](1) of this request depends on a [user-provided value](2). The [host](1) of this request depends on a [user-provided value](3).
Comment thread server/api/plextv.ts
Comment on lines +482 to +487
const response = await this.axios.get<DiscoverMetadataResponse>(
`/library/metadata/${ratingKey}`,
{
baseURL: 'https://discover.provider.plex.tv',
}
);

Check failure

Code scanning / CodeQL

Server-side request forgery

The [host](1) of this request depends on a [user-provided value](2). The [host](1) of this request depends on a [user-provided value](3).
@0xSysR3ll
Copy link
Copy Markdown
Contributor

You can ignore codeQL's review, it's not relevant in this context.

@github-actions github-actions Bot added the merge conflict Cannot merge due to merge conflicts label Feb 14, 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.

@github-actions github-actions Bot removed the merge conflict Cannot merge due to merge conflicts label Feb 16, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 16, 2026

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 Plex watchlist support: OpenAPI endpoints and docs, Express router and route wiring, Plex Discover integration and watchlist methods in the Plex API adapter, frontend Movie/TV/TitleCard UI + SWR updates, and i18n entries.

Changes

Cohort / File(s) Summary
Docs & OpenAPI
docs/using-seerr/plex/index.md, seerr-api.yml
Adds docs entry and three OpenAPI paths for Plex watchlist: POST /plex-watchlist, DELETE /plex-watchlist/{tmdbId}, GET /plex-watchlist/status/{tmdbId} with request/response schemas and error cases.
Backend: Plex API
server/api/plextv.ts
Adds Plex Discover types and methods (searchDiscover, getDiscoverMetadata, findPlexRatingKeyByTmdbId), watchlist operations (addToWatchlist, removeFromWatchlist, addToWatchlistByTmdbId, removeFromWatchlistByTmdbId, isOnWatchlist), helper findWatchlistItem, axios import, logging, and cache invalidation.
Backend: Routes
server/routes/plexWatchlist.ts, server/routes/index.ts
New authenticated router mounted at /plex-watchlist with POST/DELETE/GET handlers, request validation, user-with-plex-token helper, TMDB fallback metadata fetch, structured logging and error handling.
Frontend: Movie & TV Details
src/components/MovieDetails/index.tsx, src/components/TvDetails/index.tsx
Adds Plex-specific SWR status fetch, loading state, add/remove handlers calling new API endpoints, toasts, conditional Plex watchlist buttons (icons/spinner), and SWR cache updates.
Frontend: Cache Invalidation
src/components/TitleCard/index.tsx
Adds additional SWR mutate calls to refresh user watchlist cache after watchlist add/delete actions.
i18n
src/i18n/globalMessages.ts, src/i18n/locale/en.json
Adds localization keys addToPlexWatchlist and removeFromPlexWatchlist (English strings).

Sequence Diagram

sequenceDiagram
    participant User as User (Frontend)
    participant API as Seerr API
    participant PlexTV as PlexTvAPI
    participant Discover as Plex Discover
    participant TMDB as TMDB API

    User->>API: POST /api/v1/plex-watchlist (tmdbId, mediaType, title?)
    API->>API: Validate request, load user plexToken
    alt title/year missing
        API->>TMDB: GET /movie/:id or /tv/:id
        TMDB-->>API: title, year
    end
    API->>PlexTV: addToWatchlistByTmdbId(tmdbId, title, type, year)
    PlexTV->>Discover: searchDiscover(query, type)
    Discover-->>PlexTV: search results
    loop per result
        PlexTV->>Discover: getDiscoverMetadata(ratingKey)
        Discover-->>PlexTV: metadata with GUIDs
    end
    PlexTV->>PlexTV: match TMDB GUID -> ratingKey
    PlexTV->>PlexTV: addToWatchlist(ratingKey)
    PlexTV-->>API: success/failure
    API-->>User: 201 or error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • 0xSysR3ll
  • fallenbagel
  • gauthier-th

Poem

🐰 I hopped through logs and GUIDs bright,

Searched Discover by moonlit light,
I found the key, I gave a cheer,
A star was added, then disappeared —
Plex watchlists twinkle far and near ✨

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely describes the main feature being added: Plex watchlist management functionality, which is the primary focus of all changes across backend API, frontend UI, and localization files.

✏️ 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.

Tip

CodeRabbit can use your project's `biome` configuration to improve the quality of JS/TS/CSS/JSON code reviews.

Add a configuration file to your project to customize how CodeRabbit runs biome.

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: 8

🤖 Fix all issues with AI agents
In `@seerr-api.yml`:
- Around line 4883-4910: Update the OpenAPI operation for "Check if media is on
Plex Watchlist" (the GET operation with summary "Check if media is on Plex
Watchlist" and parameter name "tmdbId") to document the Plex-only runtime
restriction: add a '403' response entry to the responses block (e.g. description
"Forbidden - Plex account required" or similar) and update the operation
description to explicitly state that non‑Plex users are blocked at runtime and
will receive a 403 response.

In `@server/api/plextv.ts`:
- Around line 569-611: The code can throw if metadata.guid or item.guid is
undefined; before calling split().pop() in the TMDB-match loop (using variable
metadata and tmdbId) and the title-match loop (using variable item and
title/year), add a null/undefined guard for guid (e.g., check metadata.guid and
item.guid are truthy strings) and only then extract ratingKey; if missing, log a
debug/warn with context (label 'Plex.TV API', tmdbId/title) and continue to the
next item so functions that consume DiscoverMetadataResponse or allMetadata
won't throw a TypeError when guid is absent.
- Around line 479-506: getDiscoverMetadata currently only sends the X-Plex-Token
header which can return non-JSON (XML) from Plex; update the axios.get call in
getDiscoverMetadata to include an 'Accept': 'application/json' header alongside
'X-Plex-Token' so the response is explicitly JSON (mirror the header used in
searchDiscover) and adjust any logging or typing if needed to reflect the
enforced JSON response.
- Around line 758-799: Both removeFromWatchlistByTmdbId and isOnWatchlist call
getWatchlist({ size: 200 }) which can miss items if a user has >200 watchlist
entries; update these methods to paginate through the full watchlist by
repeatedly calling getWatchlist with offset and size (e.g., loop increasing
offset by size until returned items < size) and search each page for the tmdbId
before returning, or if you prefer a quicker change, replace the hardcoded size
with a configurable constant and add a clear comment documenting the limitation
and tradeoffs; update references in removeFromWatchlistByTmdbId, isOnWatchlist,
and any related error handling to use the paginated logic or the new config
value.

In `@server/routes/plexWatchlist.ts`:
- Around line 11-16: Update the watchlistRequestSchema to enforce positive
integer TMDB IDs by changing z.number() to z.number().int().positive(), and in
the route parameter guards for the DELETE and GET handlers (the handlers that
parse tmdbId from req.params) add explicit checks to reject tmdbId <= 0 (in
addition to isNaN()), returning a 400 or similar; apply the same validation
pattern to the other occurrences noted (the handlers around the sections
corresponding to lines ~155-162 and ~216-223) so all endpoints validate tmdbId
as a positive integer before calling TMDB/Plex APIs.

In `@src/components/MovieDetails/index.tsx`:
- Around line 400-456: The Plex watchlist toggle is being flipped in the finally
block causing the UI to change even on failed requests; update
onClickPlexWatchlistBtn and onClickDeletePlexWatchlistBtn so that
setTogglePlexWatchlist(...) is called only when the request succeeds (i.e.,
inside the if (response.status === 201) branch for onClickPlexWatchlistBtn and
inside the if (response.status === 204) branch for
onClickDeletePlexWatchlistBtn), leave setIsPlexWatchlistUpdating(true/false) in
place (with the false in finally), and remove the toggle call from the finally
blocks (or alternatively revalidate the server status endpoint only on success).

In `@src/components/TitleCard/index.tsx`:
- Around line 136-137: Guard the user-specific revalidation by only calling
mutate(`/api/v1/user/${user.id}/watchlist`) when user?.id is truthy and remove
the duplicate mutate of `/api/v1/discover/watchlist`; instead, call
mutate('/api/v1/discover/watchlist') once (preferably in the finally block) so
discovery is always revalidated regardless of success/failure. Locate the two
sets of mutate calls in the TitleCard component (the
mutate(`/api/v1/user/${user?.id}/watchlist`) and
mutate('/api/v1/discover/watchlist') occurrences around the success/finally
handlers) and update them to: guard the user path with user?.id and keep a
single discover mutate in finally, applying the same change to the other pair of
calls at lines 163-164.

In `@src/components/TvDetails/index.tsx`:
- Around line 432-488: The handlers onClickPlexWatchlistBtn and
onClickDeletePlexWatchlistBtn currently flip UI state in the finally block via
setTogglePlexWatchlist((prev) => !prev), causing incorrect UI when the API
errors; instead, destructure mutate from your SWR hook (where watchlist is
fetched) and move the setTogglePlexWatchlist(...) call into the success path
inside the try block (after you confirm response.status===201 or 204), and call
mutate(...) to revalidate the SWR watchlist data on success; leave error
handling and isPlexWatchlistUpdating toggles as they are but do not change UI
state in finally.
🧹 Nitpick comments (1)
docs/using-seerr/plex/index.md (1)

13-13: Consider linking this new feature to its guide.

For consistency with the linked items below, consider linking this bullet to a watchlist management guide if one exists.

Comment thread seerr-api.yml
Comment on lines +4883 to +4910
get:
summary: Check if media is on Plex Watchlist
description: Returns whether the specified media item is on the authenticated user's Plex watchlist.
tags:
- watchlist
parameters:
- in: path
name: tmdbId
description: TMDB ID of the media to check
required: true
example: '701387'
schema:
type: string
responses:
'200':
description: Watchlist status returned
content:
application/json:
schema:
type: object
properties:
isOnWatchlist:
type: boolean
example: true
'400':
description: Invalid TMDB ID
'401':
description: Unauthorized - user not logged in
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Document Plex-only restriction on status endpoint.

If non‑Plex users are blocked at runtime, the spec should advertise the 403 response and clarify the description accordingly.

Suggested OpenAPI tweak
   /plex-watchlist/status/{tmdbId}:
     get:
       summary: Check if media is on Plex Watchlist
-      description: Returns whether the specified media item is on the authenticated user's Plex watchlist.
+      description: Returns whether the specified media item is on the authenticated user's Plex watchlist. Only available for Plex users.
@@
       responses:
         '200':
           description: Watchlist status returned
@@
         '400':
           description: Invalid TMDB ID
         '401':
           description: Unauthorized - user not logged in
+        '403':
+          description: Forbidden - endpoint only available for Plex users
🤖 Prompt for AI Agents
In `@seerr-api.yml` around lines 4883 - 4910, Update the OpenAPI operation for
"Check if media is on Plex Watchlist" (the GET operation with summary "Check if
media is on Plex Watchlist" and parameter name "tmdbId") to document the
Plex-only runtime restriction: add a '403' response entry to the responses block
(e.g. description "Forbidden - Plex account required" or similar) and update the
operation description to explicitly state that non‑Plex users are blocked at
runtime and will receive a 403 response.

Comment thread server/api/plextv.ts
Comment thread server/api/plextv.ts
Comment on lines +569 to +611
// Extract ratingKey from guid (e.g., plex://movie/5d776c -> 5d776c)
// This matches Python PlexAPI: item.guid.rsplit('/', 1)[-1]
const ratingKey = metadata.guid.split('/').pop();
if (ratingKey) {
logger.info('Found match by TMDB ID', {
label: 'Plex.TV API',
extractedRatingKey: ratingKey,
itemGuid: metadata.guid,
tmdbId,
});
return ratingKey;
}
}
}
} catch (e) {
logger.warn('Failed to fetch metadata for item', {
label: 'Plex.TV API',
ratingKey: item.ratingKey,
errorMessage: e.message,
});
// Continue to next item
continue;
}
}

// If no exact TMDB match, try to match by title and year
for (const item of allMetadata) {
if (
item.title.toLowerCase() === title.toLowerCase() &&
(!year || item.year === year)
) {
// Extract ratingKey from guid (e.g., plex://movie/5d776c -> 5d776c)
const ratingKey = item.guid.split('/').pop();
if (ratingKey) {
logger.info('Found Plex ratingKey by title match', {
label: 'Plex.TV API',
tmdbId,
extractedRatingKey: ratingKey,
title: item.title,
});
return ratingKey;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential TypeError if guid is undefined.

Lines 571 and 601 call metadata.guid.split('/').pop() and item.guid.split('/').pop() respectively. The DiscoverMetadataResponse interface doesn't mark guid as required, and the Metadata type from search results also has guid as a string. If the API returns an item without a guid property, this will throw a TypeError.

🛡️ Proposed fix: Add null checks before accessing guid
          if (tmdbGuid) {
            const itemTmdbId = parseInt(tmdbGuid.id.replace('tmdb://', ''), 10);
            if (itemTmdbId === tmdbId) {
              // Extract ratingKey from guid (e.g., plex://movie/5d776c -> 5d776c)
-             const ratingKey = metadata.guid.split('/').pop();
+             const ratingKey = metadata.guid?.split('/').pop();
              if (ratingKey) {
                logger.info('Found match by TMDB ID', {
        if (
          item.title.toLowerCase() === title.toLowerCase() &&
          (!year || item.year === year)
        ) {
          // Extract ratingKey from guid (e.g., plex://movie/5d776c -> 5d776c)
-         const ratingKey = item.guid.split('/').pop();
+         const ratingKey = item.guid?.split('/').pop();
          if (ratingKey) {
            logger.info('Found Plex ratingKey by title match', {
🤖 Prompt for AI Agents
In `@server/api/plextv.ts` around lines 569 - 611, The code can throw if
metadata.guid or item.guid is undefined; before calling split().pop() in the
TMDB-match loop (using variable metadata and tmdbId) and the title-match loop
(using variable item and title/year), add a null/undefined guard for guid (e.g.,
check metadata.guid and item.guid are truthy strings) and only then extract
ratingKey; if missing, log a debug/warn with context (label 'Plex.TV API',
tmdbId/title) and continue to the next item so functions that consume
DiscoverMetadataResponse or allMetadata won't throw a TypeError when guid is
absent.

Comment thread server/api/plextv.ts
Comment on lines +11 to +16
// Validation schema for add/remove requests
const watchlistRequestSchema = z.object({
tmdbId: z.number(),
mediaType: z.enum(['movie', 'tv']),
title: z.string().optional(),
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n server/routes/plexWatchlist.ts

Repository: seerr-team/seerr

Length of output: 8064


🏁 Script executed:

#!/bin/bash
rg -n "zod" -g 'package.json' -g 'package-lock.json' -g 'pnpm-lock.yaml' -g 'yarn.lock'

Repository: seerr-team/seerr

Length of output: 1582


🌐 Web query:

Zod 4.3.6 number validation int positive methods

💡 Result:

In Zod v4.x (including 4.3.6), integer/positivity checks are methods on z.number() (chainable):

import { z } from "zod";

const s = z.number().int().positive();

Relevant ZodNumber validation methods

  • .int() → must be an integer (no decimals) [2]
  • .positive()> 0 [2]
  • Related sign/range helpers:
    • .nonnegative()>= 0 [2]
    • .negative()< 0 [2]
    • .nonpositive()<= 0 [2]
  • Comparisons:
    • .gt(n), .gte(n) (alias of .min(n)), .lt(n), .lte(n) (alias of .max(n)) [2]
  • Divisibility/step:
    • .multipleOf(n) (alias .step(n)) [2]
  • Numeric safety:
    • .finite() (rejects ±Infinity) [2]
    • .safe() (between Number.MIN_SAFE_INTEGER and Number.MAX_SAFE_INTEGER) [2]

Zod’s own docs commonly show this pattern for an integer, positive number (e.g., an age field): z.number().int().positive() [1].

Sources: Zod v4 docs/examples [1][2].


Enforce positive integer validation for TMDB IDs in schema and parameters.

The schema at line 13 uses z.number() which accepts negative, zero, and fractional values; similarly, the DELETE and GET handlers only check isNaN() and don't validate positivity. These invalid IDs propagate to TMDB/Plex API calls causing unnecessary errors. Tighten validation by adding .int().positive() to the schema and checking tmdbId <= 0 in the parameter guards.

🛡️ Proposed fix
 const watchlistRequestSchema = z.object({
-  tmdbId: z.number(),
+  tmdbId: z.number().int().positive(),
   mediaType: z.enum(['movie', 'tv']),
   title: z.string().optional(),
 });

@@
-    if (isNaN(tmdbId)) {
+    if (Number.isNaN(tmdbId) || tmdbId <= 0) {
       return next({
         status: 400,
         message: 'Invalid TMDB ID',
       });
     }

@@
-    if (isNaN(tmdbId)) {
+    if (Number.isNaN(tmdbId) || tmdbId <= 0) {
       return next({
         status: 400,
         message: 'Invalid TMDB ID',
       });
     }

Also applies to: 155-162, 216-223

🤖 Prompt for AI Agents
In `@server/routes/plexWatchlist.ts` around lines 11 - 16, Update the
watchlistRequestSchema to enforce positive integer TMDB IDs by changing
z.number() to z.number().int().positive(), and in the route parameter guards for
the DELETE and GET handlers (the handlers that parse tmdbId from req.params) add
explicit checks to reject tmdbId <= 0 (in addition to isNaN()), returning a 400
or similar; apply the same validation pattern to the other occurrences noted
(the handlers around the sections corresponding to lines ~155-162 and ~216-223)
so all endpoints validate tmdbId as a positive integer before calling TMDB/Plex
APIs.

Comment thread src/components/MovieDetails/index.tsx
Comment on lines +136 to +137
mutate(`/api/v1/user/${user?.id}/watchlist`);
mutate('/api/v1/discover/watchlist');
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard user-specific revalidation and avoid double mutate.

mutate(\/api/v1/user/${user?.id}/watchlist`)will revalidate/user/undefined/watchlistwhenuseris missing. Also,/api/v1/discover/watchlistis revalidated twice on success. Consider guarding onuser?.idand keep the discover mutate only infinally`.

Proposed fix
-      mutate('/api/v1/discover/watchlist');
       if (response.data) {
         addToast(
@@
     } finally {
       setIsUpdating(false);
       setToggleWatchlist((prevState) => !prevState);
-      mutate(`/api/v1/user/${user?.id}/watchlist`);
-      mutate('/api/v1/discover/watchlist');
+      if (user?.id) {
+        mutate(`/api/v1/user/${user.id}/watchlist`);
+      }
+      mutate('/api/v1/discover/watchlist');
     }
     } finally {
       setIsUpdating(false);
-      mutate(`/api/v1/user/${user?.id}/watchlist`);
+      if (user?.id) {
+        mutate(`/api/v1/user/${user.id}/watchlist`);
+      }
       mutate('/api/v1/discover/watchlist');
       if (mutateParent) {
         mutateParent();
       }

Also applies to: 163-164

🤖 Prompt for AI Agents
In `@src/components/TitleCard/index.tsx` around lines 136 - 137, Guard the
user-specific revalidation by only calling
mutate(`/api/v1/user/${user.id}/watchlist`) when user?.id is truthy and remove
the duplicate mutate of `/api/v1/discover/watchlist`; instead, call
mutate('/api/v1/discover/watchlist') once (preferably in the finally block) so
discovery is always revalidated regardless of success/failure. Locate the two
sets of mutate calls in the TitleCard component (the
mutate(`/api/v1/user/${user?.id}/watchlist`) and
mutate('/api/v1/discover/watchlist') occurrences around the success/finally
handlers) and update them to: guard the user path with user?.id and keep a
single discover mutate in finally, applying the same change to the other pair of
calls at lines 163-164.

Comment thread src/components/TvDetails/index.tsx
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

🤖 Fix all issues with AI agents
In `@server/api/plextv.ts`:
- Line 5: Add a 10s timeout to the two axios.put calls that send updates to the
Plex watchlist in plextv.ts: update the axios.put(...) invocations that perform
the Plex watchlist updates to include { timeout: 10000, ... } (use the same
DEFAULT_ROLLING_BUFFER value semantics) so the request options include timeout:
10000; ensure both axios.put calls are updated consistently.

Comment thread server/api/plextv.ts
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.

🧹 Nitpick comments (2)
src/components/TvDetails/index.tsx (1)

38-51: Inconsistent icon import aliasing between MovieDetails and TvDetails.

The icon imports use different aliasing patterns across the two files:

  • MovieDetails: StarIcon (outline) + StarIconSolid (solid)
  • TvDetails: StarIconOutline (outline) + StarIcon (solid)

While the visual behavior is identical (outline for add, solid for remove), the inconsistent naming reduces code readability and could cause confusion during future maintenance.

♻️ Suggested alignment with MovieDetails pattern
 import {
   ChevronDownIcon,
-  StarIcon as StarIconOutline,
+  StarIcon,
 } from '@heroicons/react/24/outline';
 import {
   ArrowRightCircleIcon,
   CogIcon,
   ExclamationTriangleIcon,
   EyeSlashIcon,
   FilmIcon,
   MinusCircleIcon,
   PlayIcon,
-  StarIcon,
+  StarIcon as StarIconSolid,
 } from '@heroicons/react/24/solid';

Then update usages on lines 749 and 767 accordingly.

server/api/plextv.ts (1)

478-497: Missing Accept: application/json header in getDiscoverMetadata.

The searchDiscover method explicitly sets Accept: 'application/json' (line 459), but getDiscoverMetadata doesn't include this header. While the ExternalAPI base class sets Accept: application/json in the constructor, consistency within the same API surface area is recommended, especially since this method was flagged in a previous review.

🔧 Add Accept header for consistency
       const response = await this.axios.get<DiscoverMetadataResponse>(
         `/library/metadata/${ratingKey}`,
         {
           baseURL: 'https://discover.provider.plex.tv',
+          headers: {
+            Accept: 'application/json',
+          },
         }
       );

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.

🧹 Nitpick comments (1)
server/api/plextv.ts (1)

478-500: Consider URL-encoding ratingKey in the path.

While ratingKey values come from Plex API responses (not direct user input), applying encodeURIComponent() to path parameters is a defensive practice that prevents unexpected behavior if the ratingKey format changes.

🛡️ Proposed fix
       const response = await this.axios.get<DiscoverMetadataResponse>(
-        `/library/metadata/${ratingKey}`,
+        `/library/metadata/${encodeURIComponent(ratingKey)}`,
         {

@gwlsn
Copy link
Copy Markdown
Author

gwlsn commented Feb 16, 2026

Ready for merge. Retested after resolving conflicts and implementing CodeRabbit fixes and confirmed working. Ignoring codeQL review as @0xSysR3ll suggested. Thanks!

@github-actions github-actions Bot added the merge conflict Cannot merge due to merge conflicts label Mar 14, 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.

@gwlsn gwlsn force-pushed the feat-plex-watchlist branch from 2d6e3e9 to 7c31984 Compare March 21, 2026 04:44
@github-actions github-actions Bot removed the merge conflict Cannot merge due to merge conflicts label Mar 21, 2026
gwlsn and others added 4 commits March 21, 2026 04:51
Add comprehensive Plex watchlist support allowing users to add and remove
movies and TV shows from their Plex watchlist directly from Seerr.

Backend Changes:
- Implement PlexTvAPI methods for watchlist operations
  - addToWatchlist() and removeFromWatchlist() using Plex Discover API
  - findPlexRatingKeyByTmdbId() with TMDB ID matching via metadata
  - isOnWatchlist() to check current watchlist status
  - Use fresh axios instance to avoid ExternalAPI header conflicts
- Add /api/v1/plex-watchlist endpoints (POST, DELETE, GET status)
- Implement duplicate prevention checking before adding items
- Add cache invalidation after watchlist modifications
- Extract ratingKey from Plex guid format (plex://movie/ID)

Frontend Changes:
- Add Plex watchlist buttons to MovieDetails and TvDetails pages
- Position buttons between Blacklist and Trailer buttons
- Use unfilled amber star icon for add, filled amber star for remove
- Implement real-time watchlist status checking via useSWR
- Add state management for watchlist updates and loading states
- Add i18n support with tooltips for user guidance
- Hide Jellyfin watchlist button for Plex users on TitleCard

API Documentation:
- Update OpenAPI spec with new Plex watchlist endpoints
- Document request/response schemas for watchlist operations

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Fix TypeScript build error by using ZodError.issues instead of the
non-existent .errors property. Fix CodeQL SSRF alerts by using
standalone axios with hardcoded URLs instead of the inherited
ExternalAPI instance, and validate ratingKey format.
Reverts the CI fix commit changes in plextv.ts that switched from
this.axios to standalone axios for CodeQL SSRF warnings. The maintainer
confirmed these can be ignored, and the change broke functionality by
losing the Accept: application/json header from the ExternalAPI base
class, causing Plex to potentially return XML instead of JSON.

Also fixes the watchlist toggle button state by moving
setTogglePlexWatchlist from finally blocks (which flip state even on
errors) to success-only branches with explicit values.
- Replace size=200 getWatchlist calls with paginated findWatchlistItem
  helper that iterates in pages of 20, staying within Plex API limits
- Remove togglePlexWatchlist state and useEffect sync in favor of
  deriving button state directly from SWR data (plexWatchlistData)
- Use SWR mutate for optimistic updates on add/remove actions
- Add 10s timeout to standalone axios.put calls for watchlist mutations
- Align TvDetails star icon aliasing with MovieDetails pattern
  (StarIcon for outline, StarIconSolid for solid)
- Add explicit Accept: application/json header to getDiscoverMetadata
  for consistency with searchDiscover
@gwlsn gwlsn force-pushed the feat-plex-watchlist branch from 7c31984 to 1f27f33 Compare March 21, 2026 04:51
@gwlsn gwlsn closed this Mar 21, 2026
@gwlsn gwlsn deleted the feat-plex-watchlist branch March 21, 2026 04:52
@clifford64
Copy link
Copy Markdown

Why was this feature removed and branch deleted? I was really looking forward to this functionality.

@clifford64
Copy link
Copy Markdown

@gwlsn Can you please provide any closure on what went down with this?

@gwlsn
Copy link
Copy Markdown
Author

gwlsn commented Apr 9, 2026

It's AI slop. I'm a big fan of this project and it deserves code written by people who actually know what they're doing.

@clifford64
Copy link
Copy Markdown

It's AI slop. I'm a big fan of this project and it deserves code written by people who actually know what they're doing.

@gwlsn

I have cloned the changes and have been working on them as well. I think the functionality is great and don't really see any immediate issues with it. I have been testing the functionality as well and it's been working great for me. Would you be willing to open this back up and continue to work on it together? It may have had some of it generated by AI, but if it works and is properly disclosed, I don't see the problem.

If not, would you have any issues if I submit this with some of the updates and changes I have made while citing your work to ensure you get credit as well?

@gauthier-th
Copy link
Copy Markdown
Member

I have cloned the changes and have been working on them as well. I think the functionality is great and don't really see any immediate issues with it. I have been testing the functionality as well and it's been working great for me.

It's not as simple as "it works for me". AI-generated code can often introduce subtle bugs, poor design patterns, or inconsistent styles that make long-term maintenance difficult and reduce overall code quality. For the sake of the project's future stability and readability, we require that all contributions meet our established coding standards and demonstrate clear developer oversight. Fully AI-authored PRs are not accepted.

@clifford64
Copy link
Copy Markdown

It's not as simple as "it works for me". AI-generated code can often introduce subtle bugs, poor design patterns, or inconsistent styles that make long-term maintenance difficult and reduce overall code quality. For the sake of the project's future stability and readability, we require that all contributions meet our established coding standards and demonstrate clear developer oversight. Fully AI-authored PRs are not accepted.

Wouldn't that be the point of the review process to ensure it meets those standards before it gets implemented?

@gauthier-th
Copy link
Copy Markdown
Member

It's not as simple as "it works for me". AI-generated code can often introduce subtle bugs, poor design patterns, or inconsistent styles that make long-term maintenance difficult and reduce overall code quality. For the sake of the project's future stability and readability, we require that all contributions meet our established coding standards and demonstrate clear developer oversight. Fully AI-authored PRs are not accepted.

Wouldn't that be the point of the review process to ensure it meets those standards before it gets implemented?

The review process is meant to review good code written by humans, not to review AI slope. Code review is not foolproof, and we don't have the same trust in AI-generated code.
We also have a limited amount of time to dedicate to the project, so taking several hours to review thousands of AI-generated lines of code made only in a few minutes for each PR is unrealistic.

@github-actions

This comment was marked as off-topic.

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.

8 participants