mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2025-12-24 02:39:18 -05:00
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:
@@ -59,6 +59,16 @@ export const SortOptionsIterable = [
|
||||
|
||||
export type SortOptions = (typeof SortOptionsIterable)[number];
|
||||
|
||||
export interface TmdbCertificationResponse {
|
||||
certifications: {
|
||||
[country: string]: {
|
||||
certification: string;
|
||||
meaning?: string;
|
||||
order?: number;
|
||||
}[];
|
||||
};
|
||||
}
|
||||
|
||||
interface DiscoverMovieOptions {
|
||||
page?: number;
|
||||
includeAdult?: boolean;
|
||||
@@ -78,6 +88,10 @@ interface DiscoverMovieOptions {
|
||||
sortBy?: SortOptions;
|
||||
watchRegion?: string;
|
||||
watchProviders?: string;
|
||||
certification?: string;
|
||||
certificationGte?: string;
|
||||
certificationLte?: string;
|
||||
certificationCountry?: string;
|
||||
}
|
||||
|
||||
interface DiscoverTvOptions {
|
||||
@@ -100,6 +114,10 @@ interface DiscoverTvOptions {
|
||||
watchRegion?: string;
|
||||
watchProviders?: string;
|
||||
withStatus?: string; // Returning Series: 0 Planned: 1 In Production: 2 Ended: 3 Cancelled: 4 Pilot: 5
|
||||
certification?: string;
|
||||
certificationGte?: string;
|
||||
certificationLte?: string;
|
||||
certificationCountry?: string;
|
||||
}
|
||||
|
||||
class TheMovieDb extends ExternalAPI {
|
||||
@@ -477,6 +495,10 @@ class TheMovieDb extends ExternalAPI {
|
||||
voteCountLte,
|
||||
watchProviders,
|
||||
watchRegion,
|
||||
certification,
|
||||
certificationGte,
|
||||
certificationLte,
|
||||
certificationCountry,
|
||||
}: DiscoverMovieOptions = {}): Promise<TmdbSearchMovieResponse> => {
|
||||
try {
|
||||
const defaultFutureDate = new Date(
|
||||
@@ -523,6 +545,10 @@ class TheMovieDb extends ExternalAPI {
|
||||
'vote_count.lte': voteCountLte,
|
||||
watch_region: watchRegion,
|
||||
with_watch_providers: watchProviders,
|
||||
certification: certification,
|
||||
'certification.gte': certificationGte,
|
||||
'certification.lte': certificationLte,
|
||||
certification_country: certificationCountry,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -552,6 +578,10 @@ class TheMovieDb extends ExternalAPI {
|
||||
watchProviders,
|
||||
watchRegion,
|
||||
withStatus,
|
||||
certification,
|
||||
certificationGte,
|
||||
certificationLte,
|
||||
certificationCountry,
|
||||
}: DiscoverTvOptions = {}): Promise<TmdbSearchTvResponse> => {
|
||||
try {
|
||||
const defaultFutureDate = new Date(
|
||||
@@ -599,6 +629,10 @@ class TheMovieDb extends ExternalAPI {
|
||||
with_watch_providers: watchProviders,
|
||||
watch_region: watchRegion,
|
||||
with_status: withStatus,
|
||||
certification: certification,
|
||||
'certification.gte': certificationGte,
|
||||
'certification.lte': certificationLte,
|
||||
certification_country: certificationCountry,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -987,6 +1021,35 @@ class TheMovieDb extends ExternalAPI {
|
||||
}
|
||||
}
|
||||
|
||||
public getMovieCertifications =
|
||||
async (): Promise<TmdbCertificationResponse> => {
|
||||
try {
|
||||
const data = await this.get<TmdbCertificationResponse>(
|
||||
'/certification/movie/list',
|
||||
{},
|
||||
604800 // 7 days
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch movie certifications: ${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
public getTvCertifications = async (): Promise<TmdbCertificationResponse> => {
|
||||
try {
|
||||
const data = await this.get<TmdbCertificationResponse>(
|
||||
'/certification/tv/list',
|
||||
{},
|
||||
604800 // 7 days
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch TV certifications: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public async getKeywordDetails({
|
||||
keywordId,
|
||||
}: {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user