feat: per-user parental controls (content rating limits)#2415
feat: per-user parental controls (content rating limits)#2415ProgenyAlpha wants to merge 1 commit intoseerr-team:developfrom
Conversation
9cbb6f4 to
d4bda48
Compare
Admin-enforced content rating limits per user: - Max movie rating (MPAA: G through NC-17) - Max TV rating (US Parental Guidelines: TV-Y through TV-MA) - Block unrated content toggle (fail-closed) Filtering applied to all discover routes and search with parallel TMDB certification lookups. Backfills from next page when filtering drops results below 15. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
d4bda48 to
f8ee51d
Compare
|
Isnt this the same as |
I wasn't aware of #2275 because it wasn't linked to any of the issues, so it didn't come up when I scoped out existing work before starting. But probably wouldn't have matter as I technically started this back in December 2025 and wanted to use it for an extensive period of time before submitting it. That said, the implementations are architecturally different. #2275 makes live TMDB API calls per-item at request time to fetch certifications, which adds latency to every discovery and search page and scales poorly with TMDB rate limits. This PR uses a static rating hierarchy; filtering is instant with zero external API overhead. A few other things this PR brings to the table:
Happy to collaborate or consolidate, the static rating approach could drop right into #2275 as a performance improvement if the maintainers prefer that path. |
Description
Adds admin-enforced per-user content rating limits (parental controls). Admins can set maximum movie (MPAA) and TV (US Parental Guidelines) ratings per user, plus a "block unrated" toggle for fail-closed filtering. Restricted users see no indication that filtering exists.
Screenshot
Admin view: User > Settings > Parental Controls
How It Works
Admin Sets Limits
A new Parental Controls tab appears in user settings (admin-only — restricted users cannot see or modify their own limits). Admins choose:
Limits are stored as new columns on
UserSettings(maxMovieRating,maxTvRating,blockUnrated) with TypeORM migrations for both PostgreSQL and SQLite.Discover Filtering (Two-Layer)
certification.lte/certification_countryquery params remove rated content above the limit at the API level — no extra requests neededblockUnratedusers, results that slipped through (unrated content TMDB doesn't filter) are caught server-side by fetching each item's US certification and checking against the hierarchySearch Filtering
Search results don't support TMDB's certification params, so all filtering is server-side. Certifications are fetched in parallel via
Promise.allSettledfor each result, then filtered against the user's limits.Backfill
When post-filtering drops a page below 15 results, the next TMDB page is automatically fetched and filtered to prevent sparse/empty pages. This applies to both discover and search routes.
Fail-Closed Design
blockUnratedis true, allowed when falseRating Hierarchies
Single source of truth in
server/constants/contentRatings.ts:Scoped to US ratings — TMDB's certification data is most complete for the US market.
Files Changed
server/constants/contentRatings.tsserver/entity/UserSettings.tsmaxMovieRating,maxTvRating,blockUnratedserver/migration/server/routes/discover.tscertificationLte, post-filter for unrated, backfillserver/routes/search.tsserver/routes/user/usersettings.tssrc/.../UserParentalControlsSettings/src/i18n/locale/en.jsonTesting
blockUnratedhides content without US certificationpnpm buildpasses clean (bothbuild:nextandbuild:server)AI Disclosure
This PR was developed with Claude Code (Claude Opus 4.6), with human review, testing, and architectural direction at every step. All code was verified against existing codebase patterns and deployed before submission.
Checklist
pnpm buildpnpm i18n:extract