feat(api): add excludeKeywords parameter to discovery queries (#1908)

Signed-off-by: 0xsysr3ll <0xsysr3ll@pm.me>
This commit is contained in:
0xsysr3ll
2025-09-16 21:32:39 +02:00
committed by GitHub
parent e9f2f4490f
commit cd479d0d17
6 changed files with 43 additions and 0 deletions

View File

@@ -5198,6 +5198,12 @@ paths:
schema: schema:
type: string type: string
example: 1,2 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 - in: query
name: sortBy name: sortBy
schema: schema:
@@ -5518,6 +5524,12 @@ paths:
schema: schema:
type: string type: string
example: 1,2 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 - in: query
name: sortBy name: sortBy
schema: schema:

View File

@@ -86,6 +86,7 @@ interface DiscoverMovieOptions {
genre?: string; genre?: string;
studio?: string; studio?: string;
keywords?: string; keywords?: string;
excludeKeywords?: string;
sortBy?: SortOptions; sortBy?: SortOptions;
watchRegion?: string; watchRegion?: string;
watchProviders?: string; watchProviders?: string;
@@ -111,6 +112,7 @@ interface DiscoverTvOptions {
genre?: string; genre?: string;
network?: number; network?: number;
keywords?: string; keywords?: string;
excludeKeywords?: string;
sortBy?: SortOptions; sortBy?: SortOptions;
watchRegion?: string; watchRegion?: string;
watchProviders?: string; watchProviders?: string;
@@ -495,6 +497,7 @@ class TheMovieDb extends ExternalAPI implements TvShowProvider {
genre, genre,
studio, studio,
keywords, keywords,
excludeKeywords,
withRuntimeGte, withRuntimeGte,
withRuntimeLte, withRuntimeLte,
voteAverageGte, voteAverageGte,
@@ -545,6 +548,7 @@ class TheMovieDb extends ExternalAPI implements TvShowProvider {
with_genres: genre, with_genres: genre,
with_companies: studio, with_companies: studio,
with_keywords: keywords, with_keywords: keywords,
without_keywords: excludeKeywords,
'with_runtime.gte': withRuntimeGte, 'with_runtime.gte': withRuntimeGte,
'with_runtime.lte': withRuntimeLte, 'with_runtime.lte': withRuntimeLte,
'vote_average.gte': voteAverageGte, 'vote_average.gte': voteAverageGte,
@@ -577,6 +581,7 @@ class TheMovieDb extends ExternalAPI implements TvShowProvider {
genre, genre,
network, network,
keywords, keywords,
excludeKeywords,
withRuntimeGte, withRuntimeGte,
withRuntimeLte, withRuntimeLte,
voteAverageGte, voteAverageGte,
@@ -628,6 +633,7 @@ class TheMovieDb extends ExternalAPI implements TvShowProvider {
with_genres: genre, with_genres: genre,
with_networks: network, with_networks: network,
with_keywords: keywords, with_keywords: keywords,
without_keywords: excludeKeywords,
'with_runtime.gte': withRuntimeGte, 'with_runtime.gte': withRuntimeGte,
'with_runtime.lte': withRuntimeLte, 'with_runtime.lte': withRuntimeLte,
'vote_average.gte': voteAverageGte, 'vote_average.gte': voteAverageGte,

View File

@@ -61,6 +61,7 @@ const QueryFilterOptions = z.object({
studio: z.coerce.string().optional(), studio: z.coerce.string().optional(),
genre: z.coerce.string().optional(), genre: z.coerce.string().optional(),
keywords: z.coerce.string().optional(), keywords: z.coerce.string().optional(),
excludeKeywords: z.coerce.string().optional(),
language: z.coerce.string().optional(), language: z.coerce.string().optional(),
withRuntimeGte: z.coerce.string().optional(), withRuntimeGte: z.coerce.string().optional(),
withRuntimeLte: z.coerce.string().optional(), withRuntimeLte: z.coerce.string().optional(),
@@ -90,6 +91,7 @@ discoverRoutes.get('/movies', async (req, res, next) => {
try { try {
const query = ApiQuerySchema.parse(req.query); const query = ApiQuerySchema.parse(req.query);
const keywords = query.keywords; const keywords = query.keywords;
const excludeKeywords = query.excludeKeywords;
const data = await tmdb.getDiscoverMovies({ const data = await tmdb.getDiscoverMovies({
page: Number(query.page), page: Number(query.page),
@@ -105,6 +107,7 @@ discoverRoutes.get('/movies', async (req, res, next) => {
? new Date(query.primaryReleaseDateGte).toISOString().split('T')[0] ? new Date(query.primaryReleaseDateGte).toISOString().split('T')[0]
: undefined, : undefined,
keywords, keywords,
excludeKeywords,
withRuntimeGte: query.withRuntimeGte, withRuntimeGte: query.withRuntimeGte,
withRuntimeLte: query.withRuntimeLte, withRuntimeLte: query.withRuntimeLte,
voteAverageGte: query.voteAverageGte, voteAverageGte: query.voteAverageGte,
@@ -381,6 +384,7 @@ discoverRoutes.get('/tv', async (req, res, next) => {
try { try {
const query = ApiQuerySchema.parse(req.query); const query = ApiQuerySchema.parse(req.query);
const keywords = query.keywords; const keywords = query.keywords;
const excludeKeywords = query.excludeKeywords;
const data = await tmdb.getDiscoverTv({ const data = await tmdb.getDiscoverTv({
page: Number(query.page), page: Number(query.page),
sortBy: query.sortBy as SortOptions, sortBy: query.sortBy as SortOptions,
@@ -395,6 +399,7 @@ discoverRoutes.get('/tv', async (req, res, next) => {
: undefined, : undefined,
originalLanguage: query.language, originalLanguage: query.language,
keywords, keywords,
excludeKeywords,
withRuntimeGte: query.withRuntimeGte, withRuntimeGte: query.withRuntimeGte,
withRuntimeLte: query.withRuntimeLte, withRuntimeLte: query.withRuntimeLte,
voteAverageGte: query.voteAverageGte, voteAverageGte: query.voteAverageGte,

View File

@@ -33,6 +33,7 @@ const messages = defineMessages('components.Discover.FilterSlideover', {
studio: 'Studio', studio: 'Studio',
genres: 'Genres', genres: 'Genres',
keywords: 'Keywords', keywords: 'Keywords',
excludeKeywords: 'Exclude Keywords',
originalLanguage: 'Original Language', originalLanguage: 'Original Language',
runtimeText: '{minValue}-{maxValue} minute runtime', runtimeText: '{minValue}-{maxValue} minute runtime',
ratingText: 'Ratings between {minValue} and {maxValue}', ratingText: 'Ratings between {minValue} and {maxValue}',
@@ -181,6 +182,19 @@ const FilterSlideover = ({
updateQueryParams('keywords', value?.map((v) => v.value).join(',')); updateQueryParams('keywords', value?.map((v) => v.value).join(','));
}} }}
/> />
<span className="text-lg font-semibold">
{intl.formatMessage(messages.excludeKeywords)}
</span>
<KeywordSelector
defaultValue={currentFilters.excludeKeywords}
isMulti
onChange={(value) => {
updateQueryParams(
'excludeKeywords',
value?.map((v) => v.value).join(',')
);
}}
/>
<span className="text-lg font-semibold"> <span className="text-lg font-semibold">
{intl.formatMessage(messages.originalLanguage)} {intl.formatMessage(messages.originalLanguage)}
</span> </span>

View File

@@ -99,6 +99,7 @@ export const QueryFilterOptions = z.object({
studio: z.string().optional(), studio: z.string().optional(),
genre: z.string().optional(), genre: z.string().optional(),
keywords: z.string().optional(), keywords: z.string().optional(),
excludeKeywords: z.string().optional(),
language: z.string().optional(), language: z.string().optional(),
withRuntimeGte: z.string().optional(), withRuntimeGte: z.string().optional(),
withRuntimeLte: z.string().optional(), withRuntimeLte: z.string().optional(),
@@ -161,6 +162,10 @@ export const prepareFilterValues = (
filterValues.keywords = values.keywords; filterValues.keywords = values.keywords;
} }
if (values.excludeKeywords) {
filterValues.excludeKeywords = values.excludeKeywords;
}
if (values.language) { if (values.language) {
filterValues.language = values.language; filterValues.language = values.language;
} }

View File

@@ -78,6 +78,7 @@
"components.Discover.FilterSlideover.activefilters": "{count, plural, one {# Active Filter} other {# Active Filters}}", "components.Discover.FilterSlideover.activefilters": "{count, plural, one {# Active Filter} other {# Active Filters}}",
"components.Discover.FilterSlideover.certification": "Content Rating", "components.Discover.FilterSlideover.certification": "Content Rating",
"components.Discover.FilterSlideover.clearfilters": "Clear Active Filters", "components.Discover.FilterSlideover.clearfilters": "Clear Active Filters",
"components.Discover.FilterSlideover.excludeKeywords": "Exclude Keywords",
"components.Discover.FilterSlideover.filters": "Filters", "components.Discover.FilterSlideover.filters": "Filters",
"components.Discover.FilterSlideover.firstAirDate": "First Air Date", "components.Discover.FilterSlideover.firstAirDate": "First Air Date",
"components.Discover.FilterSlideover.from": "From", "components.Discover.FilterSlideover.from": "From",