Skip to content
This repository was archived by the owner on Feb 15, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions overseerr-api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4263,6 +4263,11 @@ paths:
schema:
type: string
example: 18
- in: query
name: filterGenre
schema:
type: string
example: 28
- in: query
name: studio
schema:
Expand Down Expand Up @@ -4552,6 +4557,11 @@ paths:
schema:
type: string
example: 18
- in: query
name: filterGenre
schema:
type: string
example: 28
- in: query
name: network
schema:
Expand Down
6 changes: 6 additions & 0 deletions server/api/themoviedb/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ interface DiscoverMovieOptions {
voteCountLte?: string;
originalLanguage?: string;
genre?: string;
filterGenre?: string;
studio?: string;
keywords?: string;
sortBy?: SortOptions;
Expand All @@ -90,6 +91,7 @@ interface DiscoverTvOptions {
includeEmptyReleaseDate?: boolean;
originalLanguage?: string;
genre?: string;
filterGenre?: string;
network?: number;
keywords?: string;
sortBy?: SortOptions;
Expand Down Expand Up @@ -460,6 +462,7 @@ class TheMovieDb extends ExternalAPI {
primaryReleaseDateLte,
originalLanguage,
genre,
filterGenre,
studio,
keywords,
withRuntimeGte,
Expand Down Expand Up @@ -506,6 +509,7 @@ class TheMovieDb extends ExternalAPI {
? defaultFutureDate
: primaryReleaseDateLte,
with_genres: genre,
without_genres: filterGenre,
with_companies: studio,
with_keywords: keywords,
'with_runtime.gte': withRuntimeGte,
Expand Down Expand Up @@ -534,6 +538,7 @@ class TheMovieDb extends ExternalAPI {
includeEmptyReleaseDate = false,
originalLanguage,
genre,
filterGenre,
network,
keywords,
withRuntimeGte,
Expand Down Expand Up @@ -580,6 +585,7 @@ class TheMovieDb extends ExternalAPI {
: this.originalLanguage,
include_null_first_air_dates: includeEmptyReleaseDate,
with_genres: genre,
without_genres: filterGenre,
with_networks: network,
with_keywords: keywords,
'with_runtime.gte': withRuntimeGte,
Expand Down
6 changes: 6 additions & 0 deletions server/entity/UserSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ export class UserSettings {
@Column({ nullable: true })
public originalLanguage?: string;

@Column({ nullable: true })
public filterTvGenresDefault?: string;

@Column({ nullable: true })
public filterMovieGenresDefault?: string;

@Column({ nullable: true })
public pgpKey?: string;

Expand Down
2 changes: 2 additions & 0 deletions server/interfaces/api/settingsInterfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export interface PublicSettingsResponse {
series4kEnabled: boolean;
region: string;
originalLanguage: string;
filterMovieGenresDefault: string;
filterTvGenresDefault: string;
partialRequestsEnabled: boolean;
cacheImages: boolean;
vapidPublic: string;
Expand Down
2 changes: 2 additions & 0 deletions server/interfaces/api/userSettingsInterfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export interface UserSettingsGeneralResponse {
locale?: string;
region?: string;
originalLanguage?: string;
filterMovieGenresDefault?: string;
filterTvGenresDefault?: string;
movieQuotaLimit?: number;
movieQuotaDays?: number;
tvQuotaLimit?: number;
Expand Down
4 changes: 4 additions & 0 deletions server/lib/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ export interface MainSettings {
newPlexLogin: boolean;
region: string;
originalLanguage: string;
filterTvGenresDefault: string;
filterMovieGenresDefault: string;
trustProxy: boolean;
partialRequestsEnabled: boolean;
locale: string;
Expand Down Expand Up @@ -298,6 +300,8 @@ class Settings {
newPlexLogin: true,
region: '',
originalLanguage: '',
filterTvGenresDefault: '',
filterMovieGenresDefault: '',
trustProxy: false,
partialRequestsEnabled: true,
locale: 'en',
Expand Down
49 changes: 49 additions & 0 deletions server/routes/discover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const QueryFilterOptions = z.object({
firstAirDateLte: z.coerce.string().optional(),
studio: z.coerce.string().optional(),
genre: z.coerce.string().optional(),
filterGenre: z.coerce.string().optional(),
keywords: z.coerce.string().optional(),
language: z.coerce.string().optional(),
withRuntimeGte: z.coerce.string().optional(),
Expand All @@ -80,12 +81,36 @@ discoverRoutes.get('/movies', async (req, res, next) => {
try {
const query = QueryFilterOptions.parse(req.query);
const keywords = query.keywords;

// Handle user default excluded genres, resolve genres
let filterGenre = query.filterGenre;
if (filterGenre === 'none') {
filterGenre = undefined;
} else if (
filterGenre === undefined &&
req.user?.settings?.filterMovieGenresDefault
) {
filterGenre = req.user.settings.filterMovieGenresDefault;
}
if (query.genre && filterGenre) {
const explicitGenres = query.genre.split(',');
const excludedGenres = filterGenre.split(',');
const resolvedExclusions = excludedGenres.filter(
(id) => !explicitGenres.includes(id)
);
filterGenre =
resolvedExclusions.length > 0
? resolvedExclusions.join(',')
: undefined;
}

const data = await tmdb.getDiscoverMovies({
page: Number(query.page),
sortBy: query.sortBy as SortOptions,
language: req.locale ?? query.language,
originalLanguage: query.language,
genre: query.genre,
filterGenre,
studio: query.studio,
primaryReleaseDateLte: query.primaryReleaseDateLte
? new Date(query.primaryReleaseDateLte).toISOString().split('T')[0]
Expand Down Expand Up @@ -357,11 +382,35 @@ discoverRoutes.get('/tv', async (req, res, next) => {
try {
const query = QueryFilterOptions.parse(req.query);
const keywords = query.keywords;

// Handle user default excluded genres, resolve genres
let filterGenre = query.filterGenre;
if (filterGenre === 'none') {
filterGenre = undefined;
} else if (
filterGenre === undefined &&
req.user?.settings?.filterTvGenresDefault
) {
filterGenre = req.user.settings.filterTvGenresDefault;
}
if (query.genre && filterGenre) {
const explicitGenres = query.genre.split(',');
const excludedGenres = filterGenre.split(',');
const resolvedExclusions = excludedGenres.filter(
(id) => !explicitGenres.includes(id)
);
filterGenre =
resolvedExclusions.length > 0
? resolvedExclusions.join(',')
: undefined;
}

const data = await tmdb.getDiscoverTv({
page: Number(query.page),
sortBy: query.sortBy as SortOptions,
language: req.locale ?? query.language,
genre: query.genre,
filterGenre,
network: query.network ? Number(query.network) : undefined,
firstAirDateLte: query.firstAirDateLte
? new Date(query.firstAirDateLte).toISOString().split('T')[0]
Expand Down
9 changes: 9 additions & 0 deletions server/routes/user/usersettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ userSettingsRoutes.get<{ id: string }, UserSettingsGeneralResponse>(
locale: user.settings?.locale,
region: user.settings?.region,
originalLanguage: user.settings?.originalLanguage,
filterTvGenresDefault: user.settings?.filterTvGenresDefault,
filterMovieGenresDefault: user.settings?.filterMovieGenresDefault,
movieQuotaLimit: user.movieQuotaLimit,
movieQuotaDays: user.movieQuotaDays,
tvQuotaLimit: user.tvQuotaLimit,
Expand Down Expand Up @@ -116,6 +118,8 @@ userSettingsRoutes.post<
locale: req.body.locale,
region: req.body.region,
originalLanguage: req.body.originalLanguage,
filterMovieGenresDefault: req.body.filterMovieGenresDefault,
filterTvGenresDefault: req.body.filterTvGenresDefault,
watchlistSyncMovies: req.body.watchlistSyncMovies,
watchlistSyncTv: req.body.watchlistSyncTv,
});
Expand All @@ -124,6 +128,9 @@ userSettingsRoutes.post<
user.settings.locale = req.body.locale;
user.settings.region = req.body.region;
user.settings.originalLanguage = req.body.originalLanguage;
user.settings.filterMovieGenresDefault =
req.body.filterMovieGenresDefault;
user.settings.filterTvGenresDefault = req.body.filterTvGenresDefault;
user.settings.watchlistSyncMovies = req.body.watchlistSyncMovies;
user.settings.watchlistSyncTv = req.body.watchlistSyncTv;
}
Expand All @@ -136,6 +143,8 @@ userSettingsRoutes.post<
locale: user.settings.locale,
region: user.settings.region,
originalLanguage: user.settings.originalLanguage,
filterMovieGenresDefault: user.settings.filterMovieGenresDefault,
filterTvGenresDefault: user.settings.filterTvGenresDefault,
watchlistSyncMovies: user.settings.watchlistSyncMovies,
watchlistSyncTv: user.settings.watchlistSyncTv,
});
Expand Down
7 changes: 6 additions & 1 deletion src/components/Discover/DiscoverMovies/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import FilterSlideover from '@app/components/Discover/FilterSlideover';
import useDiscover from '@app/hooks/useDiscover';
import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams';
import { useUser } from '@app/hooks/useUser';
import Error from '@app/pages/_error';
import { BarsArrowDownIcon, FunnelIcon } from '@heroicons/react/24/solid';
import type { SortOptions as TMDBSortOptions } from '@server/api/themoviedb';
Expand Down Expand Up @@ -47,6 +48,7 @@ const DiscoverMovies = () => {
const intl = useIntl();
const router = useRouter();
const updateQueryParams = useUpdateQueryParams({});
const { user } = useUser();

const preparedFilters = prepareFilterValues(router.query);

Expand Down Expand Up @@ -124,7 +126,10 @@ const DiscoverMovies = () => {
<FunnelIcon />
<span>
{intl.formatMessage(messages.activefilters, {
count: countActiveFilters(preparedFilters),
count: countActiveFilters(
preparedFilters,
!!user?.settings?.filterMovieGenresDefault
),
})}
</span>
</Button>
Expand Down
7 changes: 6 additions & 1 deletion src/components/Discover/DiscoverTv/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import FilterSlideover from '@app/components/Discover/FilterSlideover';
import useDiscover from '@app/hooks/useDiscover';
import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams';
import { useUser } from '@app/hooks/useUser';
import Error from '@app/pages/_error';
import { BarsArrowDownIcon, FunnelIcon } from '@heroicons/react/24/solid';
import type { SortOptions as TMDBSortOptions } from '@server/api/themoviedb';
Expand Down Expand Up @@ -46,6 +47,7 @@ const SortOptions: Record<string, TMDBSortOptions> = {
const DiscoverTv = () => {
const intl = useIntl();
const router = useRouter();
const { user } = useUser();
const [showFilters, setShowFilters] = useState(false);
const preparedFilters = prepareFilterValues(router.query);
const updateQueryParams = useUpdateQueryParams({});
Expand Down Expand Up @@ -122,7 +124,10 @@ const DiscoverTv = () => {
<FunnelIcon />
<span>
{intl.formatMessage(messages.activefilters, {
count: countActiveFilters(preparedFilters),
count: countActiveFilters(
preparedFilters,
!!user?.settings?.filterTvGenresDefault
),
})}
</span>
</Button>
Expand Down
57 changes: 55 additions & 2 deletions src/components/Discover/FilterSlideover/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
useBatchUpdateQueryParams,
useUpdateQueryParams,
} from '@app/hooks/useUpdateQueryParams';
import { useUser } from '@app/hooks/useUser';
import { resolveGenreConflicts } from '@app/utils/genreHelpers';
import { XCircleIcon } from '@heroicons/react/24/outline';
import { defineMessages, useIntl } from 'react-intl';
import Datepicker from 'react-tailwindcss-datepicker-sct';
Expand All @@ -29,6 +31,7 @@ const messages = defineMessages({
to: 'To',
studio: 'Studio',
genres: 'Genres',
filterGenres: 'Exclude genres',
keywords: 'Keywords',
originalLanguage: 'Original Language',
runtimeText: '{minValue}-{maxValue} minute runtime',
Expand Down Expand Up @@ -56,6 +59,7 @@ const FilterSlideover = ({
}: FilterSlideoverProps) => {
const intl = useIntl();
const { currentSettings } = useSettings();
const { user } = useUser();
const updateQueryParams = useUpdateQueryParams({});
const batchUpdateQueryParams = useBatchUpdateQueryParams({});

Expand All @@ -64,12 +68,24 @@ const FilterSlideover = ({
const dateLte =
type === 'movie' ? 'primaryReleaseDateLte' : 'firstAirDateLte';

const userDefaultfilterGenres =
type === 'movie'
? user?.settings?.filterMovieGenresDefault
: user?.settings?.filterTvGenresDefault;

const filterGenresValue =
currentFilters.filterGenre !== undefined
? currentFilters.filterGenre === 'none'
? ''
: currentFilters.filterGenre
: userDefaultfilterGenres;

return (
<SlideOver
show={show}
title={intl.formatMessage(messages.filters)}
subText={intl.formatMessage(messages.activefilters, {
count: countActiveFilters(currentFilters),
count: countActiveFilters(currentFilters, !!userDefaultfilterGenres),
})}
onClose={() => onClose()}
>
Expand Down Expand Up @@ -146,7 +162,44 @@ const FilterSlideover = ({
defaultValue={currentFilters.genre}
isMulti
onChange={(value) => {
updateQueryParams('genre', value?.map((v) => v.value).join(','));
const selectedGenres = value?.map((v) => v.value.toString()) || [];
const result = resolveGenreConflicts(
selectedGenres,
filterGenresValue
);

if (result.hasConflicts) {
batchUpdateQueryParams({
genre: result.changingList,
filterGenre: result.otherList || 'none',
});
} else {
updateQueryParams('genre', result.changingList);
}
}}
/>
<span className="text-lg font-semibold">
{intl.formatMessage(messages.filterGenres)}
</span>
<GenreSelector
type={type}
defaultValue={filterGenresValue}
isMulti
onChange={(value) => {
const filterGenres = value?.map((v) => v.value.toString()) || [];
const result = resolveGenreConflicts(
filterGenres,
currentFilters.genre
);

if (result.hasConflicts) {
batchUpdateQueryParams({
genre: result.otherList,
filterGenre: result.changingList || 'none',
});
} else {
updateQueryParams('filterGenre', result.changingList || 'none');
}
}}
/>
<span className="text-lg font-semibold">
Expand Down
Loading