feat: add content certification/age-rating filter (#1418)

* feat(api): add TMDB certifications endpoint and discover certification params

re #501

* feat(discover): add certification/age-rating filter to movies and series

Add generic and US-only certification selector components, update Discover FilterSlideover, add
certification options to query constants

re #501

* fix(certificationselector): fix linter warning from useEffect missing dependency

* fix(jellyseerr-api.yml): prettier formatting

* chore(translation keys): run pnpm i18n:extract

* fix(certificationselector): change query destructure to Zod omit, fix translations, fix formatting

* style: fix whitespace with prettier
This commit is contained in:
Dillion
2025-05-06 12:55:02 -05:00
committed by GitHub
parent e5b77b2688
commit 149d79e540
10 changed files with 766 additions and 4 deletions

View File

@@ -72,16 +72,25 @@ const QueryFilterOptions = z.object({
watchProviders: z.coerce.string().optional(),
watchRegion: z.coerce.string().optional(),
status: z.coerce.string().optional(),
certification: z.coerce.string().optional(),
certificationGte: z.coerce.string().optional(),
certificationLte: z.coerce.string().optional(),
certificationCountry: z.coerce.string().optional(),
certificationMode: z.enum(['exact', 'range']).optional(),
});
export type FilterOptions = z.infer<typeof QueryFilterOptions>;
const ApiQuerySchema = QueryFilterOptions.omit({
certificationMode: true,
});
discoverRoutes.get('/movies', async (req, res, next) => {
const tmdb = createTmdbWithRegionLanguage(req.user);
try {
const query = QueryFilterOptions.parse(req.query);
const query = ApiQuerySchema.parse(req.query);
const keywords = query.keywords;
const data = await tmdb.getDiscoverMovies({
page: Number(query.page),
sortBy: query.sortBy as SortOptions,
@@ -104,6 +113,10 @@ discoverRoutes.get('/movies', async (req, res, next) => {
voteCountLte: query.voteCountLte,
watchProviders: query.watchProviders,
watchRegion: query.watchRegion,
certification: query.certification,
certificationGte: query.certificationGte,
certificationLte: query.certificationLte,
certificationCountry: query.certificationCountry,
});
const media = await Media.getRelatedMedia(
@@ -362,7 +375,7 @@ discoverRoutes.get('/tv', async (req, res, next) => {
const tmdb = createTmdbWithRegionLanguage(req.user);
try {
const query = QueryFilterOptions.parse(req.query);
const query = ApiQuerySchema.parse(req.query);
const keywords = query.keywords;
const data = await tmdb.getDiscoverTv({
page: Number(query.page),
@@ -387,6 +400,10 @@ discoverRoutes.get('/tv', async (req, res, next) => {
watchProviders: query.watchProviders,
watchRegion: query.watchRegion,
withStatus: query.status,
certification: query.certification,
certificationGte: query.certificationGte,
certificationLte: query.certificationLte,
certificationCountry: query.certificationCountry,
});
const media = await Media.getRelatedMedia(

View File

@@ -401,6 +401,48 @@ router.get('/watchproviders/tv', async (req, res, next) => {
}
});
router.get(
'/certifications/movie',
isAuthenticated(),
async (req, res, next) => {
const tmdb = new TheMovieDb();
try {
const certifications = await tmdb.getMovieCertifications();
return res.status(200).json(certifications);
} catch (e) {
logger.error('Something went wrong retrieving movie certifications', {
label: 'API',
errorMessage: e.message,
});
return next({
status: 500,
message: 'Unable to retrieve movie certifications.',
});
}
}
);
router.get('/certifications/tv', isAuthenticated(), async (req, res, next) => {
const tmdb = new TheMovieDb();
try {
const certifications = await tmdb.getTvCertifications();
return res.status(200).json(certifications);
} catch (e) {
logger.debug('Something went wrong retrieving TV certifications', {
label: 'API',
errorMessage: e.message,
});
return next({
status: 500,
message: 'Unable to retrieve TV certifications.',
});
}
});
router.get('/', (_req, res) => {
return res.status(200).json({
api: 'Jellyseerr API',