* feat(requests): enforce per-profile rating limits in discovery - Resolve each profile's max content rating and filter discovery, detail, and browse results against it, failing closed on missing ratings - Reject request submissions for titles above the viewer's ceiling - Add TMDB GetCertification backed by release_dates/content_ratings with a long-lived cache and singleflight - Push certification.lte to TMDB for studio/network/genre browse as a cost pre-filter - Backfill restricted section pages from a fixed window of TMDB pages to keep carousels populated and pagination stable * fix(requests): address discovery rating review findings - Preserve backfill overflow: sections use plain TMDB cursor semantics plus an additive next_page field instead of fixed windows, so an early stop never drops allowed titles from unconsumed pages (bit hardest at permissive R/TV-MA ceilings). - Bound cold-path cost: DiscoverAll backfills at most 2 TMDB pages per section (vs 5 for a direct section request), capping worst-case cold certification hydration at 240 lookups instead of 600. - Keep the TMDB prefilter a superset: rank-3 ceilings now push down certification.lte=NC-17/TV-MA rather than R, so titles the local ladder allows can't vanish upstream unrecoverably. - Fail closed on foreign certifications: enforcement-path lookups use new US-only pickers (a Canadian PG no longer reads as US PG), while the display path keeps its any-country fallback. US multi-entry disagreements prefer the theatrical/real rating over festival NR. - Detach shared certification fetches from the first caller's context (WithoutCancel + 30s bound) so one disconnecting client can't fail the singleflight result for concurrent waiters. - Advertise enforcement via rating_restrictions_enforced on /requests/status so clients can feature-detect instead of version-sniffing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(requests): harden rating enforcement per second review pass - GetDetail gates on the US-only enforcement certification (cached GetCertification) instead of the display rating, whose any-country fallback let a foreign "PG" pass the US ladder. - pickUSMovieCertification takes the strictest recognized US rating when multiple release entries disagree ([PG, R] -> R); entry order is not meaningful and enforcement must not admit a title on its most lenient certificate. - Certification singleflight uses DoChan so a canceled caller returns ctx.Err() immediately instead of blocking up to 30s on the detached shared fetch (which still completes for surviving waiters). - Viewer rating ceiling resolves once per request and threads through discover/browse/detail enrichment (enrichPageWithCeiling); DiscoverAll drops from 12 scope resolutions per load to 1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
45 lines
1.4 KiB
Go
45 lines
1.4 KiB
Go
package requests
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/Silo-Server/silo-server/internal/access"
|
|
)
|
|
|
|
// accessEntitlements resolves a requester's effective playback-quality ceiling
|
|
// (account + profile caps combined) via the shared access resolver.
|
|
type accessEntitlements struct {
|
|
resolver *access.Resolver
|
|
}
|
|
|
|
// NewAccessEntitlements wraps the shared access resolver as an EntitlementResolver.
|
|
func NewAccessEntitlements(resolver *access.Resolver) EntitlementResolver {
|
|
return accessEntitlements{resolver: resolver}
|
|
}
|
|
|
|
func (e accessEntitlements) MaxPlaybackQuality(ctx context.Context, userID int, profileID string) (string, error) {
|
|
scope, err := e.resolveScope(ctx, userID, profileID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return scope.MaxPlaybackQuality, nil
|
|
}
|
|
|
|
// MaxContentRating implements ContentRatingResolver so request discovery
|
|
// honors the profile's parental rating ceiling.
|
|
func (e accessEntitlements) MaxContentRating(ctx context.Context, userID int, profileID string) (string, error) {
|
|
scope, err := e.resolveScope(ctx, userID, profileID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return scope.MaxContentRating, nil
|
|
}
|
|
|
|
func (e accessEntitlements) resolveScope(ctx context.Context, userID int, profileID string) (access.Scope, error) {
|
|
return e.resolver.Resolve(ctx, access.ResolveInput{
|
|
UserID: userID,
|
|
ProfileID: profileID,
|
|
SkipPINVerification: true,
|
|
})
|
|
}
|