feat(plex): add Plex watchlist management functionality#2179
feat(plex): add Plex watchlist management functionality#2179gwlsn wants to merge 10 commits intoseerr-team:developfrom
Conversation
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>
4cb9369 to
a912f56
Compare
|
Hi, |
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. |
|
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 Why did you close this ? |
Sorry, I closed this by mistake. Would you mind reopening it? |
| 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 Critical
| 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 Critical
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.
|
You can ignore codeQL's review, it's not relevant in this context. |
|
This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged. |
Adopt blocklist naming from upstream while preserving Plex watchlist functionality from the feature branch.
📝 WalkthroughWalkthroughAdds 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
Sequence DiagramsequenceDiagram
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 + 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
PlexTV->>Discover: getDiscoverMetadata(ratingKey)
Discover-->>PlexTV: metadata with GUIDs
PlexTV->>PlexTV: match TMDB GUID -> ratingKey
PlexTV->>PlexTV: addToWatchlist(ratingKey)
PlexTV-->>API: success/failure
API-->>User: 201 or error
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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.
| public async getDiscoverMetadata( | ||
| ratingKey: string | ||
| ): Promise<DiscoverMetadataResponse> { | ||
| // Validate ratingKey format (Plex rating keys are alphanumeric hex strings) | ||
| if (!/^[a-zA-Z0-9]+$/.test(ratingKey)) { | ||
| throw new Error(`Invalid ratingKey format: ${ratingKey}`); | ||
| } | ||
|
|
||
| try { | ||
| // Use standalone axios to make the hardcoded host explicit for static analysis | ||
| const response = await axios.get<DiscoverMetadataResponse>( | ||
| `https://discover.provider.plex.tv/library/metadata/${encodeURIComponent(ratingKey)}`, | ||
| { | ||
| headers: { | ||
| 'X-Plex-Token': this.authToken, | ||
| }, | ||
| } | ||
| ); | ||
| return response.data; | ||
| } catch (e) { | ||
| logger.error('Failed to get Plex Discover metadata', { | ||
| label: 'Plex.TV API', | ||
| errorMessage: e.message, | ||
| ratingKey, | ||
| }); | ||
| throw e; | ||
| } | ||
| } |
There was a problem hiding this comment.
Missing Accept: application/json header may cause unexpected response format.
The searchDiscover method includes Accept: 'application/json' header (line 460), but getDiscoverMetadata only sends X-Plex-Token. If Plex defaults to XML, this could cause parsing failures.
🔧 Proposed fix: Add Accept header
const response = await axios.get<DiscoverMetadataResponse>(
`https://discover.provider.plex.tv/library/metadata/${encodeURIComponent(ratingKey)}`,
{
headers: {
'X-Plex-Token': this.authToken,
+ Accept: 'application/json',
},
}
);🤖 Prompt for AI Agents
In `@server/api/plextv.ts` around lines 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.
| // 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; | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| // Validation schema for add/remove requests | ||
| const watchlistRequestSchema = z.object({ | ||
| tmdbId: z.number(), | ||
| mediaType: z.enum(['movie', 'tv']), | ||
| title: z.string().optional(), | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n server/routes/plexWatchlist.tsRepository: 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()(betweenNumber.MIN_SAFE_INTEGERandNumber.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.
| mutate(`/api/v1/user/${user?.id}/watchlist`); | ||
| mutate('/api/v1/discover/watchlist'); |
There was a problem hiding this comment.
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.
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.
There was a problem hiding this comment.
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.
- 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
There was a problem hiding this comment.
🧹 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: MissingAccept: application/jsonheader ingetDiscoverMetadata.The
searchDiscovermethod explicitly setsAccept: 'application/json'(line 459), butgetDiscoverMetadatadoesn't include this header. While theExternalAPIbase class setsAccept: application/jsonin 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', + }, } );
- 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
There was a problem hiding this comment.
🧹 Nitpick comments (1)
server/api/plextv.ts (1)
478-500: Consider URL-encodingratingKeyin the path.While
ratingKeyvalues come from Plex API responses (not direct user input), applyingencodeURIComponent()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)}`, {
|
Ready for merge. Retested after resolving conflicts and implementing CodeRabbit fixes and confirmed working. Ignoring codeQL review as @0xSysR3ll suggested. Thanks! |
Description
Add Plex watchlist support allowing users to add and remove movies and TV shows from their Plex watchlist directly from Seerr. I have included the button UI element on the Movie/TV show pages instead of the TitleCard (as is found in Jellyfin/Emby) as I find I more frequently want to add things to my Plex watchlist from that screen after seeing score/reviews and description. There is also a blacklist button there so I feel it fits with the overall design language.
The adding/remove from Plex watchlist relies on undocumented API calls as detailed here.
Backend Changes:
Frontend Changes:
This patch addresses request #2082
AI Disclosure
The majority of the code in this patch was generated by Claude Code with human supervision and revision. The above PR/Commit was also summarized by Claude Code.
How Has This Been Tested?
Tested locally with my personal Plex Media Server. Added and removed items from Plex watchlist through Seerr. Removed a watchlist item on Seerr that was added through official Plex app and vice versa to ensure compatibility with underlying Plex system. Tested with blacklisted and non-blacklisted items. As intended, button not visible on blacklisted item.
Screenshots / Logs (if applicable)
Checklist:
pnpm buildpnpm i18n:extractEDIT: Updated to conform to new pull request template
Summary by CodeRabbit
New Features
Documentation
Localization