feat: per-user maturity rating caps (admin-controlled, server-enforced)#3163
feat: per-user maturity rating caps (admin-controlled, server-enforced)#3163emmick4 wants to merge 1 commit into
Conversation
📝 WalkthroughWalkthroughAdds server-enforced per-user TMDB certification rating caps ( ChangesPer-user maturity rating caps feature
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: ... }
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx (1)
85-88: 🏗️ Heavy liftAvoid 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
📒 Files selected for processing (23)
CHANGELOG.mdpackage.jsonserver/api/jellyfin.tsserver/api/themoviedb/index.tsserver/entity/MediaRequest.tsserver/entity/User.tsserver/interfaces/api/userSettingsInterfaces.tsserver/lib/ratings.test.tsserver/lib/ratings.tsserver/middleware/ratingFilter.tsserver/migration/postgres/1780300000000-AddMaturityRatingCaps.tsserver/migration/sqlite/1780300000000-AddMaturityRatingCaps.tsserver/routes/auth.tsserver/routes/discover.tsserver/routes/movie.tsserver/routes/request.tsserver/routes/search.tsserver/routes/tv.tsserver/routes/user/index.tsserver/routes/user/usersettings.tssrc/components/UserProfile/UserSettings/UserGeneralSettings/index.tsxsrc/hooks/useUser.tssrc/i18n/locale/en.json
| .catch((e) => { | ||
| logger.debug('Rating cap result filter failed; returning unfiltered', { | ||
| label: 'Ratings', | ||
| errorMessage: e.message, | ||
| }); | ||
| originalJson(body); | ||
| }); |
There was a problem hiding this comment.
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.
| user.maxMovieRating = req.body.maxMovieRating ?? null; | ||
| user.maxTvRating = req.body.maxTvRating ?? null; | ||
| user.ratingBlockUnrated = req.body.ratingBlockUnrated ?? null; |
There was a problem hiding this comment.
🧩 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 -80Repository: seerr-team/seerr
Length of output: 178
🏁 Script executed:
rg -n -A 60 'settings/main' seerr-api.yml | grep -A 60 'userId' | head -100Repository: 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 -80Repository: seerr-team/seerr
Length of output: 1623
🏁 Script executed:
rg -n 'maxMovieRating|maxTvRating|ratingBlockUnrated' seerr-api.yml | head -20Repository: seerr-team/seerr
Length of output: 42
🏁 Script executed:
sed -n '141,250p' seerr-api.ymlRepository: seerr-team/seerr
Length of output: 3028
🏁 Script executed:
wc -l seerr-api.yml && sed -n '141,300p' seerr-api.yml | tail -50Repository: seerr-team/seerr
Length of output: 1297
🏁 Script executed:
rg -n 'maxMovieRating|maxTvRating|ratingBlockUnrated' seerr-api.ymlRepository: seerr-team/seerr
Length of output: 42
🏁 Script executed:
cat -n server/interfaces/api/userSettingsInterfaces.tsRepository: seerr-team/seerr
Length of output: 1643
🏁 Script executed:
sed -n '100,170p' server/routes/user/usersettings.tsRepository: seerr-team/seerr
Length of output: 2822
🏁 Script executed:
sed -n '75,130p' src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsxRepository: seerr-team/seerr
Length of output: 2250
🏁 Script executed:
sed -n '170,210p' src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsxRepository: 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.
e2a5469 to
2f3a638
Compare
There was a problem hiding this comment.
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
📒 Files selected for processing (22)
docs/using-seerr/users/editing-users.mdserver/api/jellyfin.tsserver/api/themoviedb/index.tsserver/entity/MediaRequest.tsserver/entity/User.tsserver/interfaces/api/userSettingsInterfaces.tsserver/lib/ratings.test.tsserver/lib/ratings.tsserver/middleware/ratingFilter.tsserver/migration/postgres/1780300000000-AddMaturityRatingCaps.tsserver/migration/sqlite/1780300000000-AddMaturityRatingCaps.tsserver/routes/auth.tsserver/routes/discover.tsserver/routes/movie.tsserver/routes/request.tsserver/routes/search.tsserver/routes/tv.tsserver/routes/user/index.tsserver/routes/user/usersettings.tssrc/components/UserProfile/UserSettings/UserGeneralSettings/index.tsxsrc/hooks/useUser.tssrc/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
| :::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. | ||
| ::: |
There was a problem hiding this comment.
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.
| :::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.
2f3a638 to
9604558
Compare
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.
9604558 to
9984e4c
Compare
There was a problem hiding this comment.
🧹 Nitpick comments (2)
server/routes/discover.ts (1)
319-368: ⚡ Quick winInconsistent rating cap enforcement: uses
new TheMovieDb()instead ofcreateTmdbWithRegionLanguage(req.user).This route (and similarly
/tv/network/:networkIdat line 629 and/keyword/:keywordId/moviesat 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 incorrecttotalResults/totalPagescounts.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 winConsider stricter typing for the
typeprop.The
typeprop is currently typed asstring, 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
📒 Files selected for processing (23)
docs/using-seerr/users/editing-users.mdserver/api/jellyfin.tsserver/api/themoviedb/index.tsserver/entity/MediaRequest.tsserver/entity/User.tsserver/interfaces/api/userSettingsInterfaces.tsserver/lib/ratings.test.tsserver/lib/ratings.tsserver/middleware/ratingFilter.tsserver/migration/postgres/1780300000000-AddMaturityRatingCaps.tsserver/migration/sqlite/1780300000000-AddMaturityRatingCaps.tsserver/routes/auth.tsserver/routes/discover.tsserver/routes/movie.tsserver/routes/request.tsserver/routes/search.tsserver/routes/tv.tsserver/routes/user/index.tsserver/routes/user/usersettings.tssrc/components/Selector/USCertificationSelector.tsxsrc/components/UserProfile/UserSettings/UserGeneralSettings/index.tsxsrc/hooks/useUser.tssrc/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
|
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. |
|
👋 @emmick4, thank you for your contribution! |
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
maxMovieRating/maxTvRating/ratingBlockUnratedon 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.TheMovieDbinstance carries the cap, andgetDiscoverMovies/getDiscoverTvconstraincertificationto 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.MaxParentalRatingon 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
developtip with the project toolchain (Node 22, pnpm 10.24):pnpm typecheck— clean (server + client)pnpm lint— 0 errors (no new warnings)pnpm test— 101 passed, 0 failed (incl. newserver/lib/ratings.test.tscases for scale ordering, certification compare, admin bypass, unrated policy, and the Jellyfin score → cap mapping)pnpm build— succeedspnpm i18n:extract— no diffAlso 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
MaxParentalRatingof 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:
pnpm buildpnpm i18n:extractSummary by CodeRabbit
Release Notes
New Features
Documentation
Tests