Skip to content
Open
228 changes: 228 additions & 0 deletions seerr-api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ tags:
description: Endpoints related to getting service (Radarr/Sonarr) details.
- name: watchlist
description: Collection of media to watch later
- name: vote
description: Vote media as interested or not interested
- name: blocklist
description: Blocklisted media from discovery page.
servers:
Expand Down Expand Up @@ -254,6 +256,9 @@ components:
enableSpecialEpisodes:
type: boolean
example: false
enableVoting:
type: boolean
example: false
Comment on lines +259 to +261
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Document the voting permission and feature gate in the contract.

These new endpoints/settings are missing the key runtime constraint from the PR: voting depends on both enableVoting and the new voting permission. As written, /vote* looks like a normal authenticated surface, so API consumers have no documented reason for a 403/disabled response. Please add that requirement to the property/endpoint descriptions and document the expected rejection response(s).

📝 Suggested contract shape
        enableVoting:
           type: boolean
           example: false
+          description: Enables the voting feature server-wide. Users must still have the `VOTE` permission.

  /vote:
    post:
      summary: Create or update vote
-      description: Create a vote for the current user, or update an existing vote for the same media.
+      description: |
+        Create a vote for the current user, or update an existing vote for the same media.
+        Requires the `VOTE` permission and `enableVoting` to be enabled.
      responses:
        '200':
          description: Existing vote updated
        '201':
          description: Vote created
+        '403':
+          description: Voting is disabled or the user lacks the `VOTE` permission

Also applies to: 718-720, 4865-4982

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

In `@seerr-api.yml` around lines 259 - 261, Update the OpenAPI contract to
document that the enableVoting boolean and the new voting permission together
gate all voting endpoints: add to the enableVoting property description (symbol:
enableVoting) that voting is only available when enableVoting is true and the
caller has the voting permission, and update all /vote* endpoint descriptions
(referenced as /vote endpoints) to state the same constraint and list possible
rejection responses (403 Forbidden when feature disabled or permission missing,
plus 401 for unauthenticated), including example response bodies and status
codes; ensure the permission name used in text matches the codebase's permission
symbol (the "voting" or similarly named permission) so consumers can map it to
their auth checks.

NetworkSettings:
type: object
properties:
Expand Down Expand Up @@ -710,6 +715,70 @@ components:
initialized:
type: boolean
example: false
enableVoting:
type: boolean
example: false
Vote:
type: object
properties:
id:
type: number
example: 1
tmdbId:
type: number
example: 872585
mediaType:
type: string
enum: [movie, tv]
actionType:
type: string
enum: [interested, not_interested]
createdAt:
type: string
example: '2020-09-12T10:00:27.000Z'
updatedAt:
type: string
example: '2020-09-12T10:05:27.000Z'
VoteLookupResponse:
type: object
properties:
vote:
nullable: true
allOf:
- $ref: '#/components/schemas/Vote'
VoteHistoryResponse:
type: object
properties:
pageInfo:
$ref: '#/components/schemas/PageInfo'
results:
type: array
items:
$ref: '#/components/schemas/Vote'
VoteUpsertRequest:
type: object
properties:
tmdbId:
type: number
example: 872585
mediaType:
type: string
enum: [movie, tv]
actionType:
type: string
enum: [interested, not_interested]
required:
- tmdbId
- mediaType
- actionType
VoteUpsertResponse:
allOf:
- $ref: '#/components/schemas/Vote'
- type: object
properties:
created:
type: boolean
example: true
Comment on lines +721 to +781
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

wc -l seerr-api.yml

Repository: seerr-team/seerr

Length of output: 78


🏁 Script executed:

# Check the main range (721-781)
sed -n '721,781p' seerr-api.yml

Repository: seerr-team/seerr

Length of output: 1516


🏁 Script executed:

# Check other ranges mentioned
sed -n '4896,4908p' seerr-api.yml

Repository: seerr-team/seerr

Length of output: 345


🏁 Script executed:

# Check path params ranges
sed -n '4953,4956p' seerr-api.yml
sed -n '4976,4979p' seerr-api.yml

Repository: seerr-team/seerr

Length of output: 241


🏁 Script executed:

# Check discover/popular range
sed -n '6202,6207p' seerr-api.yml

Repository: seerr-team/seerr

Length of output: 189


Use integer for IDs and pagination fields.

The new contract models discrete values like id, tmdbId, take, skip, and page as number. In OpenAPI, this generates floating-point types in SDKs and permits invalid inputs like 1.5 for TMDB IDs or page numbers. Change these to integer.

Applies to lines 724-725, 761-763, 4896-4908, 4953-4956, 4976-4979, and 6202-6207.

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

In `@seerr-api.yml` around lines 721 - 781, The OpenAPI schemas use type: number
for discrete fields causing float generation; update the Vote schema (properties
id and tmdbId) and VoteUpsertRequest (tmdbId) to use type: integer (and ensure
examples are integers), and also change all pagination-related fields (PageInfo
and any schemas with page, take, skip) from type: number to type: integer so IDs
and pagination params are modeled as integers across the contract.

MovieResult:
type: object
required:
Expand Down Expand Up @@ -4793,6 +4862,124 @@ paths:
responses:
'204':
description: Succesfully removed watchlist item
/vote:
post:
summary: Create or update vote
description: Create a vote for the current user, or update an existing vote for the same media.
tags:
- vote
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/VoteUpsertRequest'
responses:
'200':
description: Existing vote updated
content:
application/json:
schema:
$ref: '#/components/schemas/VoteUpsertResponse'
'201':
description: Vote created
content:
application/json:
schema:
$ref: '#/components/schemas/VoteUpsertResponse'
/vote/history:
get:
summary: Get current user vote history
tags:
- vote
parameters:
- in: query
name: take
schema:
type: number
default: 20
minimum: 1
maximum: 100
- in: query
name: skip
schema:
type: number
default: 0
minimum: 0
- in: query
name: filter
schema:
type: string
enum: [all, interested, not_interested]
default: all
- in: query
name: mediaType
schema:
type: string
enum: [all, movie, tv]
default: all
- in: query
name: sort
schema:
type: string
enum: [added]
default: added
- in: query
name: sortDirection
schema:
type: string
enum: [asc, desc]
default: desc
responses:
'200':
description: Vote history returned
content:
application/json:
schema:
$ref: '#/components/schemas/VoteHistoryResponse'
/vote/{mediaType}/{tmdbId}:
get:
summary: Get current user vote for media
tags:
- vote
parameters:
- in: path
name: mediaType
required: true
schema:
type: string
enum: [movie, tv]
- in: path
name: tmdbId
required: true
schema:
type: number
responses:
'200':
description: Vote lookup returned
content:
application/json:
schema:
$ref: '#/components/schemas/VoteLookupResponse'
delete:
summary: Delete current user vote for media
tags:
- vote
parameters:
- in: path
name: mediaType
required: true
schema:
type: string
enum: [movie, tv]
- in: path
name: tmdbId
required: true
schema:
type: number
responses:
'204':
description: Vote removed
/user/{userId}/watchlist:
get:
summary: Get the Plex watchlist for a specific user
Expand Down Expand Up @@ -6005,6 +6192,47 @@ paths:
- $ref: '#/components/schemas/MovieResult'
- $ref: '#/components/schemas/TvResult'
- $ref: '#/components/schemas/PersonResult'
/discover/popular:
get:
summary: Popular on this server
description: Returns a list of popular movies and TV shows based on server vote totals.
tags:
- search
parameters:
- in: query
name: page
schema:
type: number
example: 1
default: 1
- in: query
name: language
schema:
type: string
example: en
responses:
'200':
description: Results
content:
application/json:
schema:
type: object
properties:
page:
type: number
example: 1
totalPages:
type: number
example: 1
totalResults:
type: number
example: 20
results:
type: array
items:
anyOf:
- $ref: '#/components/schemas/MovieResult'
- $ref: '#/components/schemas/TvResult'
/discover/keyword/{keywordId}/movies:
get:
summary: Get movies from keyword
Expand Down
23 changes: 15 additions & 8 deletions server/constants/discover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export enum DiscoverSliderType {
TMDB_NETWORK,
TMDB_MOVIE_STREAMING_SERVICES,
TMDB_TV_STREAMING_SERVICES,
POPULAR_ON_SERVER,
}

export const defaultSliders: Partial<DiscoverSlider>[] = [
Expand Down Expand Up @@ -50,51 +51,57 @@ export const defaultSliders: Partial<DiscoverSlider>[] = [
order: 3,
},
{
type: DiscoverSliderType.POPULAR_MOVIES,
type: DiscoverSliderType.POPULAR_ON_SERVER,
enabled: true,
isBuiltIn: true,
order: 4,
},
{
type: DiscoverSliderType.MOVIE_GENRES,
type: DiscoverSliderType.POPULAR_MOVIES,
enabled: true,
isBuiltIn: true,
order: 5,
},
{
type: DiscoverSliderType.UPCOMING_MOVIES,
type: DiscoverSliderType.MOVIE_GENRES,
enabled: true,
isBuiltIn: true,
order: 6,
},
{
type: DiscoverSliderType.STUDIOS,
type: DiscoverSliderType.UPCOMING_MOVIES,
enabled: true,
isBuiltIn: true,
order: 7,
},
{
type: DiscoverSliderType.POPULAR_TV,
type: DiscoverSliderType.STUDIOS,
enabled: true,
isBuiltIn: true,
order: 8,
},
{
type: DiscoverSliderType.TV_GENRES,
type: DiscoverSliderType.POPULAR_TV,
enabled: true,
isBuiltIn: true,
order: 9,
},
{
type: DiscoverSliderType.UPCOMING_TV,
type: DiscoverSliderType.TV_GENRES,
enabled: true,
isBuiltIn: true,
order: 10,
},
{
type: DiscoverSliderType.NETWORKS,
type: DiscoverSliderType.UPCOMING_TV,
enabled: true,
isBuiltIn: true,
order: 11,
},
{
type: DiscoverSliderType.NETWORKS,
enabled: true,
isBuiltIn: true,
order: 12,
},
];
7 changes: 7 additions & 0 deletions server/entity/DiscoverSlider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ class DiscoverSlider {
slider,
});
await sliderRepository.save(new DiscoverSlider(slider));
} else if (
existingSlider.isBuiltIn &&
typeof slider.order === 'number' &&
existingSlider.order !== slider.order
) {
existingSlider.order = slider.order;
await sliderRepository.save(existingSlider);
Comment on lines +26 to +32
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't reset persisted built-in slider order during bootstrap.

DiscoverSlider.bootstrapSliders() runs on every startup, and the discover settings route already persists admin-defined order for built-in sliders. This branch will silently overwrite those customizations on every boot. The new built-in insertion needs a one-time migration/backfill that shifts existing rows instead of mutating order here.

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

In `@server/entity/DiscoverSlider.ts` around lines 26 - 32,
DiscoverSlider.bootstrapSliders() currently overwrites persisted admin changes
by setting existingSlider.order and calling sliderRepository.save when a
built-in slider's order differs; stop mutating persisted built-in order during
startup: remove the branch that assigns existingSlider.order = slider.order and
the subsequent sliderRepository.save call in bootstrapSliders(), and instead
implement a separate one-time migration/backfill routine (not in
bootstrapSliders) that shifts existing rows to accommodate new built-in
insertions; alternatively, if immediate change is required, only set order when
existingSlider.order is null/undefined (leave non-null values untouched) and
perform any shifting logic in a dedicated migration function.

}
}
}
Expand Down
Loading
Loading