From cd479d0d1787bd8c289e111844ba3f85cb4ec23a Mon Sep 17 00:00:00 2001
From: 0xsysr3ll <31414959+0xSysR3ll@users.noreply.github.com>
Date: Tue, 16 Sep 2025 21:32:39 +0200
Subject: [PATCH] feat(api): add excludeKeywords parameter to discovery queries
(#1908)
Signed-off-by: 0xsysr3ll <0xsysr3ll@pm.me>
---
jellyseerr-api.yml | 12 ++++++++++++
server/api/themoviedb/index.ts | 6 ++++++
server/routes/discover.ts | 5 +++++
src/components/Discover/FilterSlideover/index.tsx | 14 ++++++++++++++
src/components/Discover/constants.ts | 5 +++++
src/i18n/locale/en.json | 1 +
6 files changed, 43 insertions(+)
diff --git a/jellyseerr-api.yml b/jellyseerr-api.yml
index 2535059a..69484e24 100644
--- a/jellyseerr-api.yml
+++ b/jellyseerr-api.yml
@@ -5198,6 +5198,12 @@ paths:
schema:
type: string
example: 1,2
+ - in: query
+ name: excludeKeywords
+ schema:
+ type: string
+ example: 3,4
+ description: Comma-separated list of keyword IDs to exclude from results
- in: query
name: sortBy
schema:
@@ -5518,6 +5524,12 @@ paths:
schema:
type: string
example: 1,2
+ - in: query
+ name: excludeKeywords
+ schema:
+ type: string
+ example: 3,4
+ description: Comma-separated list of keyword IDs to exclude from results
- in: query
name: sortBy
schema:
diff --git a/server/api/themoviedb/index.ts b/server/api/themoviedb/index.ts
index 9fc19c72..a01f356c 100644
--- a/server/api/themoviedb/index.ts
+++ b/server/api/themoviedb/index.ts
@@ -86,6 +86,7 @@ interface DiscoverMovieOptions {
genre?: string;
studio?: string;
keywords?: string;
+ excludeKeywords?: string;
sortBy?: SortOptions;
watchRegion?: string;
watchProviders?: string;
@@ -111,6 +112,7 @@ interface DiscoverTvOptions {
genre?: string;
network?: number;
keywords?: string;
+ excludeKeywords?: string;
sortBy?: SortOptions;
watchRegion?: string;
watchProviders?: string;
@@ -495,6 +497,7 @@ class TheMovieDb extends ExternalAPI implements TvShowProvider {
genre,
studio,
keywords,
+ excludeKeywords,
withRuntimeGte,
withRuntimeLte,
voteAverageGte,
@@ -545,6 +548,7 @@ class TheMovieDb extends ExternalAPI implements TvShowProvider {
with_genres: genre,
with_companies: studio,
with_keywords: keywords,
+ without_keywords: excludeKeywords,
'with_runtime.gte': withRuntimeGte,
'with_runtime.lte': withRuntimeLte,
'vote_average.gte': voteAverageGte,
@@ -577,6 +581,7 @@ class TheMovieDb extends ExternalAPI implements TvShowProvider {
genre,
network,
keywords,
+ excludeKeywords,
withRuntimeGte,
withRuntimeLte,
voteAverageGte,
@@ -628,6 +633,7 @@ class TheMovieDb extends ExternalAPI implements TvShowProvider {
with_genres: genre,
with_networks: network,
with_keywords: keywords,
+ without_keywords: excludeKeywords,
'with_runtime.gte': withRuntimeGte,
'with_runtime.lte': withRuntimeLte,
'vote_average.gte': voteAverageGte,
diff --git a/server/routes/discover.ts b/server/routes/discover.ts
index 4fdd1167..c6dab52a 100644
--- a/server/routes/discover.ts
+++ b/server/routes/discover.ts
@@ -61,6 +61,7 @@ const QueryFilterOptions = z.object({
studio: z.coerce.string().optional(),
genre: z.coerce.string().optional(),
keywords: z.coerce.string().optional(),
+ excludeKeywords: z.coerce.string().optional(),
language: z.coerce.string().optional(),
withRuntimeGte: z.coerce.string().optional(),
withRuntimeLte: z.coerce.string().optional(),
@@ -90,6 +91,7 @@ discoverRoutes.get('/movies', async (req, res, next) => {
try {
const query = ApiQuerySchema.parse(req.query);
const keywords = query.keywords;
+ const excludeKeywords = query.excludeKeywords;
const data = await tmdb.getDiscoverMovies({
page: Number(query.page),
@@ -105,6 +107,7 @@ discoverRoutes.get('/movies', async (req, res, next) => {
? new Date(query.primaryReleaseDateGte).toISOString().split('T')[0]
: undefined,
keywords,
+ excludeKeywords,
withRuntimeGte: query.withRuntimeGte,
withRuntimeLte: query.withRuntimeLte,
voteAverageGte: query.voteAverageGte,
@@ -381,6 +384,7 @@ discoverRoutes.get('/tv', async (req, res, next) => {
try {
const query = ApiQuerySchema.parse(req.query);
const keywords = query.keywords;
+ const excludeKeywords = query.excludeKeywords;
const data = await tmdb.getDiscoverTv({
page: Number(query.page),
sortBy: query.sortBy as SortOptions,
@@ -395,6 +399,7 @@ discoverRoutes.get('/tv', async (req, res, next) => {
: undefined,
originalLanguage: query.language,
keywords,
+ excludeKeywords,
withRuntimeGte: query.withRuntimeGte,
withRuntimeLte: query.withRuntimeLte,
voteAverageGte: query.voteAverageGte,
diff --git a/src/components/Discover/FilterSlideover/index.tsx b/src/components/Discover/FilterSlideover/index.tsx
index 1f06cf0a..abdd0105 100644
--- a/src/components/Discover/FilterSlideover/index.tsx
+++ b/src/components/Discover/FilterSlideover/index.tsx
@@ -33,6 +33,7 @@ const messages = defineMessages('components.Discover.FilterSlideover', {
studio: 'Studio',
genres: 'Genres',
keywords: 'Keywords',
+ excludeKeywords: 'Exclude Keywords',
originalLanguage: 'Original Language',
runtimeText: '{minValue}-{maxValue} minute runtime',
ratingText: 'Ratings between {minValue} and {maxValue}',
@@ -181,6 +182,19 @@ const FilterSlideover = ({
updateQueryParams('keywords', value?.map((v) => v.value).join(','));
}}
/>
+
+ {intl.formatMessage(messages.excludeKeywords)}
+
+ {
+ updateQueryParams(
+ 'excludeKeywords',
+ value?.map((v) => v.value).join(',')
+ );
+ }}
+ />
{intl.formatMessage(messages.originalLanguage)}
diff --git a/src/components/Discover/constants.ts b/src/components/Discover/constants.ts
index 425ee7de..4ce5e34f 100644
--- a/src/components/Discover/constants.ts
+++ b/src/components/Discover/constants.ts
@@ -99,6 +99,7 @@ export const QueryFilterOptions = z.object({
studio: z.string().optional(),
genre: z.string().optional(),
keywords: z.string().optional(),
+ excludeKeywords: z.string().optional(),
language: z.string().optional(),
withRuntimeGte: z.string().optional(),
withRuntimeLte: z.string().optional(),
@@ -161,6 +162,10 @@ export const prepareFilterValues = (
filterValues.keywords = values.keywords;
}
+ if (values.excludeKeywords) {
+ filterValues.excludeKeywords = values.excludeKeywords;
+ }
+
if (values.language) {
filterValues.language = values.language;
}
diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json
index 7153d5a3..69573dc0 100644
--- a/src/i18n/locale/en.json
+++ b/src/i18n/locale/en.json
@@ -78,6 +78,7 @@
"components.Discover.FilterSlideover.activefilters": "{count, plural, one {# Active Filter} other {# Active Filters}}",
"components.Discover.FilterSlideover.certification": "Content Rating",
"components.Discover.FilterSlideover.clearfilters": "Clear Active Filters",
+ "components.Discover.FilterSlideover.excludeKeywords": "Exclude Keywords",
"components.Discover.FilterSlideover.filters": "Filters",
"components.Discover.FilterSlideover.firstAirDate": "First Air Date",
"components.Discover.FilterSlideover.from": "From",