Discover Query Builder
Construct TMDB discover filter params — comma/pipe AND/OR expressions and range bounds — without hand-building strings.
TMDB's discover.movie() and discover.tv()
endpoints accept dozens of filter params. Several of them (with_genres, with_keywords,
with_companies, with_watch_providers, with_watch_monetization_types, and their without_*
counterparts) expect a serialized string — comma-separated for AND, pipe-separated for OR — and
several others (vote_average.gte/.lte, with_runtime.gte/.lte, primary_release_date.gte/.lte,
etc.) are really a { gte, lte } range split across two dotted keys.
@lorenzopant/tmdb ships two layers to build these params: standalone functions for dynamic,
data-driven construction, and fluent builder classes for a fixed call chain. Both produce the same
plain DiscoverMovieParams/DiscoverTVParams object you already pass to discover.movie()/discover.tv().
import { and, or, voteAverage, DiscoverMovieQueryBuilder, DiscoverTVQueryBuilder } from "@lorenzopant/tmdb";Functional core
and() and or() join values into the string TMDB expects:
and([28, 12]); // "28,12" — match ALL genres
or([28, 12]); // "28|12" — match ANY genre
await tmdb.discover.movie({ with_genres: or([28, 12]) });A set of range functions build the .gte/.lte key pairs, omitting whichever bound you don't
pass:
voteAverage({ gte: 6 }); // { "vote_average.gte": 6 }
await tmdb.discover.movie({
...voteAverage({ gte: 6, lte: 8 }),
...runtime({ lte: 120 }),
});Available range functions: voteAverage, voteCount (shared), runtime, primaryReleaseDate,
releaseDate, certification (movie-only), airDate, firstAirDate (TV-only).
without_genres, without_keywords, without_companies, and without_watch_providers only support comma-separated (AND) exclusion on
TMDB's API — there's no pipe/OR variant, so and() is the only function you need there.
Dynamic construction
Because these are plain functions returning plain objects, they slot directly into code that builds params from an arbitrary source — a URL's search params, a form, or your own internal filter vocabulary mapped to TMDB's:
const params: DiscoverMovieParams = {};
for (const [key, value] of url.searchParams) {
switch (key) {
case "genres":
params.with_genres = or(mapGenreIds(value));
break;
case "excludeKeywords":
params.without_keywords = and(mapKeywordIds(value));
break;
case "minRating":
Object.assign(params, voteAverage({ gte: Number(value) }));
break;
}
}
const movies = await tmdb.discover.movie(params);DiscoverMovieQueryBuilder / DiscoverTVQueryBuilder
Thin, chainable sugar over the same functions above, for call sites written as a fixed sequence
of filters. Every chain method is named after the real TMDB param it sets. Call .build() to get
the plain params object, then pass it to discover.movie()/discover.tv():
const params = new DiscoverMovieQueryBuilder()
.withGenres("or", [28, 12])
.withoutKeywords([818])
.voteAverage({ gte: 6 })
.runtime({ lte: 150 })
.sortBy("popularity.desc")
.build();
const movies = await tmdb.discover.movie(params);const series = await tmdb.discover.tv(
new DiscoverTVQueryBuilder().withNetworks("or", [213]).withStatus("and", [0]).voteAverage({ gte: 7 }).sortBy("popularity.desc").build(),
);DiscoverMovieQueryBuilder and DiscoverTVQueryBuilder each only expose the methods valid for
their endpoint — movie-only filters like .runtime() or .certification() don't exist on
DiscoverTVQueryBuilder, and TV-only filters like .withNetworks() or .airDate() don't exist on
DiscoverMovieQueryBuilder.
Builders don't replace discover.movie()/discover.tv() — they only build the params object. There's no implicit .build(); call it
explicitly before passing the result to the endpoint.