Skip to content

feat: per-user maturity rating caps (admin-controlled, server-enforced)#3163

Closed
emmick4 wants to merge 1 commit into
seerr-team:developfrom
emmick4:parental-controls
Closed

feat: per-user maturity rating caps (admin-controlled, server-enforced)#3163
emmick4 wants to merge 1 commit into
seerr-team:developfrom
emmick4:parental-controls

Conversation

@emmick4

@emmick4 emmick4 commented Jun 14, 2026

Copy link
Copy Markdown

Description

Adds native, admin-controlled, server-enforced per-user content-rating (maturity) limits to Seerr — the long-requested "parental controls" capability. A user can be capped to a maximum movie certification and a maximum TV certification (US ratings); above the cap they cannot discover, view, or request titles. Admins set and lock the caps; users cannot raise their own.

Implements two long-standing open requests:

Why

Families running Jellyfin + Seerr can restrict playback per-user in Jellyfin, but Seerr itself has no per-user maturity control — a child account can browse the full TMDB catalog and request R / TV-MA titles. This closes that gap.

What's included

  • Data model: maxMovieRating / maxTvRating / ratingBlockUnrated on the User entity (TMDB US certification strings; null = no limit). Additive migrations for SQLite and Postgres; existing users default to no limit, so default behavior is unchanged.
  • Discover — enforced at the TMDB query: for a capped user the per-request TheMovieDb instance carries the cap, and getDiscoverMovies/getDiscoverTv constrain certification to the allowed US list. This returns full, correctly-filtered pages (rather than post-filtering a normal page down to a sparse list). A user may narrow within their cap (e.g. a PG-capped user picking only G) but never above it.
  • Search / trending / recommendations / watchlist (which TMDB can't certification-filter at the query): a result-filter middleware drops over-cap items. It fails open on a transient cert-lookup error so a cold cache never blanks a page — the detail and request endpoints remain the hard wall.
  • Hard wall: movie/TV detail endpoints return 403 for over-cap titles, and request creation returns 403. Enforcement lives in the API layer, so it holds against direct API calls.
  • Admin UI: Max Movie / Max Series Rating selectors + a "Block Unrated" toggle on the per-user settings page, shown only when an admin edits a non-admin user. The Discover certification picker only offers ratings at or below the viewing user's cap.
  • Jellyfin import: reads the Jellyfin user policy MaxParentalRating on login + import and maps it to the cap (non-destructive — only when Jellyfin has a limit set), so a Jellyfin-managed cap drives both viewing and requesting.

How Has This Been Tested?

Built and tested against the current develop tip with the project toolchain (Node 22, pnpm 10.24):

  • pnpm typecheck — clean (server + client)
  • pnpm lint — 0 errors (no new warnings)
  • pnpm test101 passed, 0 failed (incl. new server/lib/ratings.test.ts cases for scale ordering, certification compare, admin bypass, unrated policy, and the Jellyfin score → cap mapping)
  • pnpm build — succeeds
  • pnpm i18n:extract — no diff

Also running in production (Jellyfin + Seerr) and verified in-cluster with a capped (PG) account: Discover/genre pages return full, kid-appropriate results; G-only vs PG-only narrowing works; over-cap picks clamp to the cap; movie/TV detail + request return 403 for over-cap titles; and a Jellyfin MaxParentalRating of PG/PG-13 imports to the matching cap.

Screenshots / Logs (if applicable)

N/A — server-enforced behavior; covered by unit tests and the in-cluster verification above.

AI assistance disclosure

Per CONTRIBUTING: this change was implemented with AI assistance (Claude Code), under my direction and review. It was not opened blindly — the feature was designed, implemented, deployed to a real Jellyfin + Seerr stack, and validated end-to-end (including the in-cluster testing above) before and after each change. Happy to walk through any part of it or make changes.

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

Release Notes

  • New Features

    • Added per-user controls for maximum movie and TV ratings, plus an option to block unrated/unknown content.
    • Enforced rating caps across search, discovery, recommendations/similar, and during media requests (including 403 when disallowed).
    • Jellyfin/Emby parental-rating limits now auto-sync into the corresponding rating restrictions.
    • Admin users are exempt from these restrictions.
  • Documentation

    • Updated the user guide with setup and behavior details for rating caps.
  • Tests

    • Added automated coverage for rating extraction, cap enforcement, and Jellyfin mapping.

@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds server-enforced per-user TMDB certification rating caps (maxMovieRating, maxTvRating, ratingBlockUnrated) stored as new nullable User entity columns with database migrations for PostgreSQL and SQLite. A new ratings.ts library defines certification scales, computes effective caps with admin bypass, extracts certifications from TMDB endpoints, and validates user access to content. Caps are enforced at request creation (throwing RatingRestrictedError when exceeded), media detail routes (403 response), and discover/search results (filtered via middleware). Jellyfin parental control policies are automatically synced to Seerr user caps on login and bulk import. A user settings UI allows cap configuration with dropdowns and a checkbox.

Changes

Per-user maturity rating caps feature

Layer / File(s) Summary
Data model, migrations, and API type extensions
server/entity/User.ts, server/migration/postgres/1780300000000-AddMaturityRatingCaps.ts, server/migration/sqlite/1780300000000-AddMaturityRatingCaps.ts, server/interfaces/api/userSettingsInterfaces.ts, server/api/jellyfin.ts, server/api/themoviedb/index.ts
Adds three nullable columns (maxMovieRating, maxTvRating, ratingBlockUnrated) to User entity with reversible Postgres and SQLite migrations. Extends JellyfinUserResponse.Policy with MaxParentalRating and BlockUnratedItems fields. Adds UserSettingsGeneralResponse cap fields. Extends TheMovieDb provider with setMaturityRatingCap(), getMovieReleaseDates(), and getTvContentRatings() cached methods supporting certification lookups.
Core ratings library and tests
server/lib/ratings.ts, server/lib/ratings.test.ts
Implements TMDB movie/TV certification scales, EffectiveRatingCaps structure, getEffectiveRatingCaps() with admin bypass, extractMovieCertification() / extractTvCertification() from TMDB payloads, certification enforcement via isMovieCertificationAllowed() / isTvCertificationAllowed() with configurable unrated blocking, async bulk result filtering via filterResultsByRatingCaps(), and Jellyfin compatibility via jellyfinRatingToCaps() / applyJellyfinRatingCaps(). Comprehensive test suite validates extraction, cap computation, enforcement logic, and Jellyfin mapping.
TMDB provider rating cap support
server/api/themoviedb/index.ts
Extends discover query builders for movies and TV to enforce per-user rating caps: when set, forces certificationCountry: US, clamps certification to allowed list, and clears certification.gte/certification.lte to prevent range widening. Adds internal clampCertificationsToCap() helper and setMaturityRatingCap() public method.
Request creation enforcement and error handling
server/entity/MediaRequest.ts, server/routes/request.ts
MediaRequest.request() extracts movie/TV certification from TMDB release_dates/content_ratings after quota checks and throws RatingRestrictedError when exceeding the user's cap. POST /request route catches this error and maps to 403 alongside existing permission/quota errors.
Route-level filtering middleware and enforcement
server/middleware/ratingFilter.ts, server/routes/discover.ts, server/routes/search.ts, server/routes/movie.ts, server/routes/tv.ts
Introduces ratingCapResultFilter middleware that wraps res.json to filter results arrays via filterResultsByRatingCaps() using req.user. Registers on discover and search routers. Movie and TV detail routes add early 403 guards using certification extractors and enforcement helpers. Recommendations and similar endpoints filter mapped results before returning.
Jellyfin parental policy sync on login and import
server/routes/auth.ts, server/routes/user/index.ts
Calls applyJellyfinRatingCaps() during Jellyfin/Emby login (existing and new user flows) and bulk user import to sync parental policies to Seerr user caps before persisting.
User settings API routes, frontend UI, and documentation
server/routes/user/usersettings.ts, src/hooks/useUser.ts, src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx, src/i18n/locale/en.json, docs/using-seerr/users/editing-users.md, src/components/Selector/USCertificationSelector.tsx
Settings GET/POST routes expose and persist cap fields (admin-only write path). Frontend adds select dropdowns for movie/TV rating caps and a "Block Unrated Content" checkbox via Formik. useUser hook and i18n strings updated. Certification selector now filters options based on user caps. Documentation describes limit enforcement, Jellyfin sync on import, and admin-only restrictions.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant DiscoverRoute
  participant ratingCapResultFilter
  participant filterResultsByRatingCaps
  participant TheMovieDb

  Client->>DiscoverRoute: GET /discover/movies?page=1
  DiscoverRoute->>DiscoverRoute: query TMDB for results
  DiscoverRoute->>ratingCapResultFilter: res.json({ results: [movie1, movie2...], ... })
  ratingCapResultFilter->>filterResultsByRatingCaps: filterResultsByRatingCaps(results, req.user)
  loop for each movie
    filterResultsByRatingCaps->>TheMovieDb: getMovieReleaseDates(movieId)
    TheMovieDb-->>filterResultsByRatingCaps: TmdbMovieReleaseResult
    filterResultsByRatingCaps->>filterResultsByRatingCaps: extractMovieCertification()
    filterResultsByRatingCaps->>filterResultsByRatingCaps: isMovieCertificationAllowed(cert, user)
  end
  filterResultsByRatingCaps-->>ratingCapResultFilter: filtered items
  ratingCapResultFilter-->>Client: { results: [movie1, ...], totalResults: ... }
Loading
sequenceDiagram
  participant Client
  participant RequestRoute
  participant MediaRequest
  participant TheMovieDb
  participant RatingLib

  Client->>RequestRoute: POST /request { mediaType: MOVIE, mediaId: 12345, requestedBy: userId }
  RequestRoute->>MediaRequest: MediaRequest.request(requestedBy, mediaType, mediaId)
  MediaRequest->>TheMovieDb: getMovieDetails(movieId)
  TheMovieDb-->>MediaRequest: TmdbMovieDetails { release_dates: [{country, certification}...] }
  MediaRequest->>RatingLib: extractMovieCertification(release_dates)
  RatingLib-->>MediaRequest: certification string
  MediaRequest->>RatingLib: isMovieCertificationAllowed(cert, requestedBy)
  alt certification exceeds user cap
    RatingLib-->>MediaRequest: false
    MediaRequest-->>RequestRoute: throws RatingRestrictedError
    RequestRoute->>RequestRoute: catch RatingRestrictedError
    RequestRoute-->>Client: { status: 403, message: "Certification restricted..." }
  else certification within cap
    RatingLib-->>MediaRequest: true
    MediaRequest->>MediaRequest: create MediaRequest record
    MediaRequest-->>RequestRoute: mediaRequest
    RequestRoute-->>Client: { status: 201, mediaRequest: {...} }
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • fallenbagel
  • gauthier-th
  • M0NsTeRRR

Poem

🐇 A rabbit guards Seerr's content with care,
Rating caps filter what users can bear.
Jellyfin whispers its parental rules,
Certificates checked by the warren's tools.
Unrated mysteries? Blocked at the gate,
Fair filters ensure age-appropriate fate! 🎬

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main feature: admin-controlled, server-enforced per-user maturity rating caps, which is the core addition across all changed files.
Linked Issues check ✅ Passed The PR fully implements both linked issues: #354 requests Jellyfin parental control integration (implemented via applyJellyfinRatingCaps), and #501 requests admin-enforced age restrictions (implemented via maxMovieRating/maxTvRating fields and server-side filtering).
Out of Scope Changes check ✅ Passed All changes directly support the maturity rating caps feature: database schema, rating logic, API enforcement, UI controls, Jellyfin integration, and documentation. No unrelated changes detected.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx (1)

85-88: 🏗️ Heavy lift

Avoid manual duplication of certification scales across client/server.

Line 85–88 hardcodes rating order and relies on a “keep in sync” comment. A shared typed source for these options would prevent drift between UI selections and backend enforcement/mapping behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx` around
lines 85 - 88, The MOVIE_RATING_OPTIONS and TV_RATING_OPTIONS constants in the
UserGeneralSettings component are hardcoded and rely on a manual comment to stay
synchronized with the server-side MOVIE_RATING_SCALE and TV_RATING_SCALE
definitions, which creates a risk of drift. Instead of maintaining these as
separate hardcoded arrays, extract these rating options into a shared, typed
constants file that can be imported by both the client component and the server
code, eliminating the need for manual synchronization and ensuring the UI
selections always match the backend enforcement behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/middleware/ratingFilter.ts`:
- Around line 41-47: The catch handler in the rating filter middleware currently
calls originalJson(body) which returns the original unfiltered payload when
filtering fails, allowing users with active rating caps to bypass the
restriction. Replace this fail-open behavior with a fail-closed approach that
enforces the rating cap even when filtering throws an error. Instead of
returning the original unfiltered body, either deny the response entirely or
return a filtered/restricted version that respects the rating cap for users with
active caps.

In `@server/routes/user/usersettings.ts`:
- Around line 124-126: The OpenAPI schema in seerr-api.yml is missing
documentation for three fields that are actually being accepted and persisted in
the UserSettings endpoint: maxMovieRating, maxTvRating, and ratingBlockUnrated
(as shown in the usersettings.ts diff). Add these three fields to the
UserSettings object schema in seerr-api.yml. For maxMovieRating and maxTvRating,
use type string with nullable: true and provide examples like 'PG-13' and
'TV-14' with descriptions for maximum allowed ratings. For ratingBlockUnrated,
use type boolean with nullable: true, example true, and a description indicating
it blocks unrated content from the user.

---

Nitpick comments:
In `@src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx`:
- Around line 85-88: The MOVIE_RATING_OPTIONS and TV_RATING_OPTIONS constants in
the UserGeneralSettings component are hardcoded and rely on a manual comment to
stay synchronized with the server-side MOVIE_RATING_SCALE and TV_RATING_SCALE
definitions, which creates a risk of drift. Instead of maintaining these as
separate hardcoded arrays, extract these rating options into a shared, typed
constants file that can be imported by both the client component and the server
code, eliminating the need for manual synchronization and ensuring the UI
selections always match the backend enforcement behavior.
🪄 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: 9ac02dde-4b04-4816-9239-565eb5facbab

📥 Commits

Reviewing files that changed from the base of the PR and between 6c8527f and e2a5469.

📒 Files selected for processing (23)
  • CHANGELOG.md
  • package.json
  • server/api/jellyfin.ts
  • server/api/themoviedb/index.ts
  • server/entity/MediaRequest.ts
  • server/entity/User.ts
  • server/interfaces/api/userSettingsInterfaces.ts
  • server/lib/ratings.test.ts
  • server/lib/ratings.ts
  • server/middleware/ratingFilter.ts
  • server/migration/postgres/1780300000000-AddMaturityRatingCaps.ts
  • server/migration/sqlite/1780300000000-AddMaturityRatingCaps.ts
  • server/routes/auth.ts
  • server/routes/discover.ts
  • server/routes/movie.ts
  • server/routes/request.ts
  • server/routes/search.ts
  • server/routes/tv.ts
  • server/routes/user/index.ts
  • server/routes/user/usersettings.ts
  • src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx
  • src/hooks/useUser.ts
  • src/i18n/locale/en.json

Comment on lines +41 to +47
.catch((e) => {
logger.debug('Rating cap result filter failed; returning unfiltered', {
label: 'Ratings',
errorMessage: e.message,
});
originalJson(body);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail-open fallback can bypass rating-cap enforcement.

At Line 41, the catch path returns the original unfiltered payload. If filtering throws, restricted users can receive over-cap titles. This should fail closed for users with active caps.

Suggested fix
-import { filterResultsByRatingCaps } from '`@server/lib/ratings`';
+import {
+  filterResultsByRatingCaps,
+  getEffectiveRatingCaps,
+} from '`@server/lib/ratings`';
@@
     filterResultsByRatingCaps(
       results as { id: number; tmdbId?: number; mediaType: string }[],
       req.user
     )
       .then((filtered) => {
         originalJson({ ...(body as object), results: filtered });
       })
       .catch((e) => {
         logger.debug('Rating cap result filter failed; returning unfiltered', {
           label: 'Ratings',
           errorMessage: e.message,
         });
-        originalJson(body);
+        const caps = getEffectiveRatingCaps(req.user);
+        if (caps) {
+          return originalJson({ ...(body as object), results: [] });
+        }
+        return originalJson(body);
       });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/middleware/ratingFilter.ts` around lines 41 - 47, The catch handler in
the rating filter middleware currently calls originalJson(body) which returns
the original unfiltered payload when filtering fails, allowing users with active
rating caps to bypass the restriction. Replace this fail-open behavior with a
fail-closed approach that enforces the rating cap even when filtering throws an
error. Instead of returning the original unfiltered body, either deny the
response entirely or return a filtered/restricted version that respects the
rating cap for users with active caps.

Comment on lines +124 to +126
user.maxMovieRating = req.body.maxMovieRating ?? null;
user.maxTvRating = req.body.maxTvRating ?? null;
user.ratingBlockUnrated = req.body.ratingBlockUnrated ?? null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate API schema/docs that define POST /user/{id}/settings/main"
fd -i 'openapi|swagger|api-doc|schema' .

echo
echo "Find route + schema references for settings main and new cap fields"
rg -n -C3 --glob '*.{ts,tsx,yml,yaml,json}' '/settings/main|maxMovieRating|maxTvRating|ratingBlockUnrated'

echo
echo "Inspect required-vs-optional definitions near those fields"
rg -n -C5 --glob '*.{yml,yaml,json,ts}' 'required|maxMovieRating|maxTvRating|ratingBlockUnrated'

Repository: seerr-team/seerr

Length of output: 50373


🏁 Script executed:

rg -n -A 50 '/user/{userId}/settings/main' seerr-api.yml | grep -A 50 'post:' | head -80

Repository: seerr-team/seerr

Length of output: 178


🏁 Script executed:

rg -n -A 60 'settings/main' seerr-api.yml | grep -A 60 'userId' | head -100

Repository: seerr-team/seerr

Length of output: 2244


🏁 Script executed:

rg -n -B 5 -A 50 'UserSettings:' seerr-api.yml | grep -A 60 'type: object' | head -80

Repository: seerr-team/seerr

Length of output: 1623


🏁 Script executed:

rg -n 'maxMovieRating|maxTvRating|ratingBlockUnrated' seerr-api.yml | head -20

Repository: seerr-team/seerr

Length of output: 42


🏁 Script executed:

sed -n '141,250p' seerr-api.yml

Repository: seerr-team/seerr

Length of output: 3028


🏁 Script executed:

wc -l seerr-api.yml && sed -n '141,300p' seerr-api.yml | tail -50

Repository: seerr-team/seerr

Length of output: 1297


🏁 Script executed:

rg -n 'maxMovieRating|maxTvRating|ratingBlockUnrated' seerr-api.yml

Repository: seerr-team/seerr

Length of output: 42


🏁 Script executed:

cat -n server/interfaces/api/userSettingsInterfaces.ts

Repository: seerr-team/seerr

Length of output: 1643


🏁 Script executed:

sed -n '100,170p' server/routes/user/usersettings.ts

Repository: seerr-team/seerr

Length of output: 2822


🏁 Script executed:

sed -n '75,130p' src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx

Repository: seerr-team/seerr

Length of output: 2250


🏁 Script executed:

sed -n '170,210p' src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx

Repository: seerr-team/seerr

Length of output: 1999


Add maxMovieRating, maxTvRating, and ratingBlockUnrated to the UserSettings schema in seerr-api.yml.

The three rating cap fields are accepted and persisted by the POST endpoint but are not documented in the OpenAPI schema. While the client always sends these fields explicitly (avoiding the destructive scenario the original comment flagged), the schema should reflect the actual request contract.

Add the fields to the UserSettings object in seerr-api.yml:

maxMovieRating:
  type: string
  nullable: true
  example: 'PG-13'
  description: 'Maximum movie rating allowed for this user'
maxTvRating:
  type: string
  nullable: true
  example: 'TV-14'
  description: 'Maximum TV rating allowed for this user'
ratingBlockUnrated:
  type: boolean
  nullable: true
  example: true
  description: 'Block unrated content from this user'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/routes/user/usersettings.ts` around lines 124 - 126, The OpenAPI
schema in seerr-api.yml is missing documentation for three fields that are
actually being accepted and persisted in the UserSettings endpoint:
maxMovieRating, maxTvRating, and ratingBlockUnrated (as shown in the
usersettings.ts diff). Add these three fields to the UserSettings object schema
in seerr-api.yml. For maxMovieRating and maxTvRating, use type string with
nullable: true and provide examples like 'PG-13' and 'TV-14' with descriptions
for maximum allowed ratings. For ratingBlockUnrated, use type boolean with
nullable: true, example true, and a description indicating it blocks unrated
content from the user.

@emmick4
emmick4 force-pushed the parental-controls branch from e2a5469 to 2f3a638 Compare June 14, 2026 16:17

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/using-seerr/users/editing-users.md`:
- Around line 62-64: The tip section at lines 62-64 clarifies that parental
rating limits can be adjusted afterward, but it does not mention that these
manual edits may be overwritten when existing Jellyfin users log in again due to
the automatic Jellyfin policy sync that occurs at login. Update the tip to
explicitly state that for users imported from Jellyfin, manual adjustments to
the parental rating limits are temporary and will be reset to the Jellyfin
account settings on the next login, so admins understand these are not permanent
overrides but rather login-synced defaults from Jellyfin.
🪄 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: ae485939-5b54-480e-bf95-cbc7bef84c20

📥 Commits

Reviewing files that changed from the base of the PR and between e2a5469 and 2f3a638.

📒 Files selected for processing (22)
  • docs/using-seerr/users/editing-users.md
  • server/api/jellyfin.ts
  • server/api/themoviedb/index.ts
  • server/entity/MediaRequest.ts
  • server/entity/User.ts
  • server/interfaces/api/userSettingsInterfaces.ts
  • server/lib/ratings.test.ts
  • server/lib/ratings.ts
  • server/middleware/ratingFilter.ts
  • server/migration/postgres/1780300000000-AddMaturityRatingCaps.ts
  • server/migration/sqlite/1780300000000-AddMaturityRatingCaps.ts
  • server/routes/auth.ts
  • server/routes/discover.ts
  • server/routes/movie.ts
  • server/routes/request.ts
  • server/routes/search.ts
  • server/routes/tv.ts
  • server/routes/user/index.ts
  • server/routes/user/usersettings.ts
  • src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx
  • src/hooks/useUser.ts
  • src/i18n/locale/en.json
✅ Files skipped from review due to trivial changes (1)
  • server/interfaces/api/userSettingsInterfaces.ts
🚧 Files skipped from review as they are similar to previous changes (17)
  • server/routes/discover.ts
  • server/api/jellyfin.ts
  • server/entity/User.ts
  • server/middleware/ratingFilter.ts
  • src/i18n/locale/en.json
  • server/routes/request.ts
  • server/routes/auth.ts
  • server/api/themoviedb/index.ts
  • server/routes/user/usersettings.ts
  • server/routes/tv.ts
  • server/routes/movie.ts
  • server/routes/search.ts
  • src/hooks/useUser.ts
  • server/lib/ratings.test.ts
  • src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx
  • server/entity/MediaRequest.ts
  • server/lib/ratings.ts

Comment on lines +62 to +64
:::tip
For users imported from Jellyfin, the **Maximum allowed parental rating** configured on the Jellyfin account is imported automatically and used as the starting limit — keeping what a user can watch and what they can request in sync. You can still adjust the limits here afterward.
:::

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clarify persistence of manual edits for Jellyfin-synced caps.

Line 63 says limits can be adjusted afterward, but if Jellyfin policy sync runs on login for existing Jellyfin users, those manual edits may be overwritten on next login. Please clarify this behavior so admins know whether these fields are one-time defaults or continuously synced.

Suggested doc tweak
-For users imported from Jellyfin, the **Maximum allowed parental rating** configured on the Jellyfin account is imported automatically and used as the starting limit — keeping what a user can watch and what they can request in sync. You can still adjust the limits here afterward.
+For users imported from Jellyfin, the **Maximum allowed parental rating** configured on the Jellyfin account is imported automatically and used as the starting limit — keeping what a user can watch and what they can request in sync. You can adjust the limits here, but if Jellyfin policy sync is applied again (for example on login), Jellyfin values may overwrite manual edits.

Based on learnings from the provided review stack context, existing Jellyfin users receive cap updates during login sync.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
:::tip
For users imported from Jellyfin, the **Maximum allowed parental rating** configured on the Jellyfin account is imported automatically and used as the starting limit — keeping what a user can watch and what they can request in sync. You can still adjust the limits here afterward.
:::
:::tip
For users imported from Jellyfin, the **Maximum allowed parental rating** configured on the Jellyfin account is imported automatically and used as the starting limit — keeping what a user can watch and what they can request in sync. You can adjust the limits here, but if Jellyfin policy sync is applied again (for example on login), Jellyfin values may overwrite manual edits.
:::
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/using-seerr/users/editing-users.md` around lines 62 - 64, The tip
section at lines 62-64 clarifies that parental rating limits can be adjusted
afterward, but it does not mention that these manual edits may be overwritten
when existing Jellyfin users log in again due to the automatic Jellyfin policy
sync that occurs at login. Update the tip to explicitly state that for users
imported from Jellyfin, manual adjustments to the parental rating limits are
temporary and will be reset to the Jellyfin account settings on the next login,
so admins understand these are not permanent overrides but rather login-synced
defaults from Jellyfin.

@emmick4
emmick4 force-pushed the parental-controls branch from 2f3a638 to 9604558 Compare June 14, 2026 16:49
Adds native per-user max movie/TV certification caps so capped (e.g. child)
accounts cannot discover or request titles above their rating. Addresses
upstream issues seerr-team#354 and seerr-team#501.

Data model
- New nullable User columns: maxMovieRating, maxTvRating, ratingBlockUnrated
  (TMDB US certification strings; null = no limit). Additive TypeORM migrations
  for sqlite + postgres.

Enforcement (server is the wall)
- server/lib/ratings.ts: ordered US movie/TV scales, certification extraction +
  comparison, and Jellyfin MaxParentalRating -> cap mapping. Admins bypass.
- Discover: the cap is enforced at the TMDB query for capped users. The
  per-request TheMovieDb instance carries the cap and getDiscoverMovies/
  getDiscoverTv constrain certification to the allowed US list, so pages stay
  full and correctly filtered. A user may narrow within the cap but not above it.
- Search / trending / recommendations / watchlist (which TMDB cannot
  certification-filter at the query): a result-filter middleware drops over-cap
  items, failing open on a transient lookup error so a cold cache never blanks a
  page. The detail and request endpoints remain the hard 403 wall.
- Movie/TV detail: 403 for over-cap titles. Request creation: 403.

Admin UI
- Per-user Max Movie/Series Rating selectors + Block Unrated toggle, shown only
  when an admin edits a non-admin user. The Discover certification picker only
  offers ratings at or below the viewing user's cap.

Jellyfin integration (seerr-team#354)
- Import the cap from the Jellyfin user policy MaxParentalRating on login and on
  import-from-jellyfin (non-destructive: only when Jellyfin has a limit set).

Tests: server/lib/ratings.test.ts.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
server/routes/discover.ts (1)

319-368: ⚡ Quick win

Inconsistent rating cap enforcement: uses new TheMovieDb() instead of createTmdbWithRegionLanguage(req.user).

This route (and similarly /tv/network/:networkId at line 629 and /keyword/:keywordId/movies at line 804) bypasses query-level maturity cap enforcement. The post-filter middleware will still remove over-cap items, but users will see sparse pages with incorrect totalResults/totalPages counts.

Suggested fix
-    const tmdb = new TheMovieDb();
+    const tmdb = createTmdbWithRegionLanguage(req.user);

Apply the same fix to:

  • Line 629 in /tv/network/:networkId
  • Line 804 in /keyword/:keywordId/movies
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/routes/discover.ts` around lines 319 - 368, The routes for
`/movies/studio/:studioId`, `/tv/network/:networkId`, and
`/keyword/:keywordId/movies` are bypassing query-level maturity cap enforcement
by instantiating TheMovieDb with `new TheMovieDb()` instead of using
`createTmdbWithRegionLanguage(req.user)`. In each of these three route handlers,
replace the line `const tmdb = new TheMovieDb();` with `const tmdb =
createTmdbWithRegionLanguage(req.user);` to ensure consistent rating cap
enforcement at the query level and prevent sparse pages with incorrect
totalResults and totalPages counts.
src/components/Selector/USCertificationSelector.tsx (1)

4-5: ⚡ Quick win

Consider stricter typing for the type prop.

The type prop is currently typed as string, which allows any value. Restricting it to a union type would prevent invalid values and improve type safety.

🔧 Suggested type improvement
 interface USCertificationSelectorProps {
-  type: string;
+  type: 'movie' | 'tv';
   certification?: string;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Selector/USCertificationSelector.tsx` around lines 4 - 5, The
`type` prop in the `USCertificationSelectorProps` interface is currently typed
as `string`, which is too permissive and allows any string value. Replace the
`string` type annotation with a union type that explicitly lists the valid
certification types that this selector component accepts. This will prevent
invalid values from being passed and improve type safety throughout the
component.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@server/routes/discover.ts`:
- Around line 319-368: The routes for `/movies/studio/:studioId`,
`/tv/network/:networkId`, and `/keyword/:keywordId/movies` are bypassing
query-level maturity cap enforcement by instantiating TheMovieDb with `new
TheMovieDb()` instead of using `createTmdbWithRegionLanguage(req.user)`. In each
of these three route handlers, replace the line `const tmdb = new TheMovieDb();`
with `const tmdb = createTmdbWithRegionLanguage(req.user);` to ensure consistent
rating cap enforcement at the query level and prevent sparse pages with
incorrect totalResults and totalPages counts.

In `@src/components/Selector/USCertificationSelector.tsx`:
- Around line 4-5: The `type` prop in the `USCertificationSelectorProps`
interface is currently typed as `string`, which is too permissive and allows any
string value. Replace the `string` type annotation with a union type that
explicitly lists the valid certification types that this selector component
accepts. This will prevent invalid values from being passed and improve type
safety throughout the component.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5c657bbd-eb2c-4422-b1a8-86c372f66832

📥 Commits

Reviewing files that changed from the base of the PR and between 9604558 and 9984e4c.

📒 Files selected for processing (23)
  • docs/using-seerr/users/editing-users.md
  • server/api/jellyfin.ts
  • server/api/themoviedb/index.ts
  • server/entity/MediaRequest.ts
  • server/entity/User.ts
  • server/interfaces/api/userSettingsInterfaces.ts
  • server/lib/ratings.test.ts
  • server/lib/ratings.ts
  • server/middleware/ratingFilter.ts
  • server/migration/postgres/1780300000000-AddMaturityRatingCaps.ts
  • server/migration/sqlite/1780300000000-AddMaturityRatingCaps.ts
  • server/routes/auth.ts
  • server/routes/discover.ts
  • server/routes/movie.ts
  • server/routes/request.ts
  • server/routes/search.ts
  • server/routes/tv.ts
  • server/routes/user/index.ts
  • server/routes/user/usersettings.ts
  • src/components/Selector/USCertificationSelector.tsx
  • src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx
  • src/hooks/useUser.ts
  • src/i18n/locale/en.json
✅ Files skipped from review due to trivial changes (2)
  • docs/using-seerr/users/editing-users.md
  • src/i18n/locale/en.json
🚧 Files skipped from review as they are similar to previous changes (17)
  • server/entity/User.ts
  • server/migration/sqlite/1780300000000-AddMaturityRatingCaps.ts
  • src/hooks/useUser.ts
  • server/routes/request.ts
  • server/interfaces/api/userSettingsInterfaces.ts
  • server/middleware/ratingFilter.ts
  • server/migration/postgres/1780300000000-AddMaturityRatingCaps.ts
  • server/api/jellyfin.ts
  • server/routes/user/index.ts
  • server/routes/user/usersettings.ts
  • server/routes/auth.ts
  • server/routes/movie.ts
  • server/entity/MediaRequest.ts
  • server/routes/tv.ts
  • src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx
  • server/lib/ratings.test.ts
  • server/lib/ratings.ts

@gauthier-th

Copy link
Copy Markdown
Member

I'm closing this since there is already an open PR for this feature.

This also looks like it has been too much heavily AI-generated without enough supervision.

@fallenbagel fallenbagel added duplicate This issue or pull request already exists ai-generated labels Jun 15, 2026
@github-actions

Copy link
Copy Markdown

👋 @emmick4, thank you for your contribution!
However, this pull request has been closed because it appears to contain a significant amount of AI-generated code without sufficient human review or supervision.
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.
This pull request is also too large for effective human review. Please discuss with us on how to break down these changes into smaller, more focused PRs to ensure a thorough and efficient review process. If you'd like to revise and resubmit your changes with careful review and cleanup, we'd be happy to take another look.

@github-actions github-actions Bot locked as spam and limited conversation to collaborators Jun 15, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

ai-generated duplicate This issue or pull request already exists

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Adjustable Age Restrictions for viewing content per user [Feature Request] Limit Visible Requests By Jellyfin Parental Control Ratings

3 participants