Skip to content
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
8 changes: 8 additions & 0 deletions docs/using-seerr/settings/general.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ When enabled, media that has been blocklisted will not appear on the "Discover"

This setting is **disabled** by default.

## Hide Requested Media

When enabled, media that has been requested (but not yet available) will not appear on the "Discover" home page, or in the "Recommended" or "Similar" categories or other links on media detail pages.

Requested media will still appear in search results, however, so it is possible to locate and view hidden items by searching for them by title.

This setting is **disabled** by default.

## Allow Partial Series Requests

When enabled, users will be able to submit requests for specific seasons of TV series. If disabled, users will only be able to submit requests for all unavailable seasons.
Expand Down
3 changes: 3 additions & 0 deletions seerr-api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,9 @@ components:
hideAvailable:
type: boolean
example: false
hideRequested:
type: boolean
example: false
partialRequestsEnabled:
type: boolean
example: false
Expand Down
3 changes: 2 additions & 1 deletion server/entity/Media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ class Media {
'watchlist',
'media.id= watchlist.media and watchlist.requestedBy = :userId',
{ userId: user?.id }
) //,
)
.leftJoinAndSelect('media.requests', 'requests')

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding leftJoinAndSelect('media.requests', 'requests') in getRelatedMedia will load full MediaRequest entities for every discover/search result. Because MediaRequest has eager relations (e.g. requestedBy, modifiedBy, seasons), this can significantly increase payload size and may expose requester user details in endpoints that previously only returned mediaInfo.status. Consider returning a lightweight signal instead (e.g., relation count / existence of non-declined/non-completed requests via loadRelationCountAndMap, or a subquery that selects only request IDs/statuses) rather than selecting the full relation.

Suggested change
.leftJoinAndSelect('media.requests', 'requests')
.leftJoin('media.requests', 'requests')
.loadRelationCountAndMap('media.requestCount', 'media.requests')

Copilot uses AI. Check for mistakes.
.where(' media.tmdbId in (:...finalIds)', { finalIds })
.getMany();

Expand Down
1 change: 1 addition & 0 deletions server/interfaces/api/settingsInterfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export interface PublicSettingsResponse {
applicationUrl: string;
hideAvailable: boolean;
hideBlocklisted: boolean;
hideRequested: boolean;
localLogin: boolean;
mediaServerLogin: boolean;
movie4kEnabled: boolean;
Expand Down
4 changes: 4 additions & 0 deletions server/lib/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ export interface MainSettings {
};
hideAvailable: boolean;
hideBlocklisted: boolean;
hideRequested: boolean;
localLogin: boolean;
mediaServerLogin: boolean;
newPlexLogin: boolean;
Expand Down Expand Up @@ -184,6 +185,7 @@ interface FullPublicSettings extends PublicSettings {
applicationUrl: string;
hideAvailable: boolean;
hideBlocklisted: boolean;
hideRequested: boolean;
localLogin: boolean;
mediaServerLogin: boolean;
movie4kEnabled: boolean;
Expand Down Expand Up @@ -394,6 +396,7 @@ class Settings {
},
hideAvailable: false,
hideBlocklisted: false,
hideRequested: false,
localLogin: true,
mediaServerLogin: true,
newPlexLogin: true,
Expand Down Expand Up @@ -678,6 +681,7 @@ class Settings {
applicationUrl: this.data.main.applicationUrl,
hideAvailable: this.data.main.hideAvailable,
hideBlocklisted: this.data.main.hideBlocklisted,
hideRequested: this.data.main.hideRequested,
localLogin: this.data.main.localLogin,
mediaServerLogin: this.data.main.mediaServerLogin,
jellyfinExternalHost: this.data.jellyfin.externalHostname,
Expand Down
15 changes: 15 additions & 0 deletions src/components/MediaSlider/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,21 @@ const MediaSlider = ({
);
}

if (settings.currentSettings.hideRequested) {
titles = titles.filter((i) => {
if (i.mediaType !== 'movie' && i.mediaType !== 'tv') {
return true;
}

return (
i.mediaInfo?.status === MediaStatus.AVAILABLE ||
i.mediaInfo?.status === MediaStatus.PARTIALLY_AVAILABLE ||
!i.mediaInfo?.requests ||
i.mediaInfo.requests.length === 0
Comment on lines +91 to +95

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This requested-media filter relies on mediaInfo.requests.length without considering request status, so titles with only DECLINED (and possibly COMPLETED/FAILED) request history will be hidden as well. It would be safer to treat a title as requested only when it has an active request (e.g., status not DECLINED/COMPLETED), or derive this from mediaInfo.status if that’s the canonical state.

Suggested change
return (
i.mediaInfo?.status === MediaStatus.AVAILABLE ||
i.mediaInfo?.status === MediaStatus.PARTIALLY_AVAILABLE ||
!i.mediaInfo?.requests ||
i.mediaInfo.requests.length === 0
const hasActiveRequests =
!!i.mediaInfo?.requests &&
i.mediaInfo.requests.some(
(request) =>
request.status !== 'DECLINED' &&
request.status !== 'COMPLETED' &&
request.status !== 'FAILED'
);
return (
i.mediaInfo?.status === MediaStatus.AVAILABLE ||
i.mediaInfo?.status === MediaStatus.PARTIALLY_AVAILABLE ||
!hasActiveRequests

Copilot uses AI. Check for mistakes.
);
});
}
Comment thread
0xSysR3ll marked this conversation as resolved.

useEffect(() => {
if (
titles.length < 24 &&
Expand Down
2 changes: 1 addition & 1 deletion src/components/Search/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const Search = () => {
{
query: router.query.query,
},
{ hideAvailable: false, hideBlocklisted: false }
{ hideAvailable: false, hideBlocklisted: false, hideRequested: false }
);

if (error) {
Expand Down
25 changes: 25 additions & 0 deletions src/components/Settings/SettingsMain/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ const messages = defineMessages('components.Settings.SettingsMain', {
hideAvailable: 'Hide Available Media',
hideAvailableTip:
'Hide available media from the discover pages but not search results',
hideRequested: 'Hide Requested Media',
hideRequestedTip:
'Hide media that has been requested from the discover pages but not search results',
cacheImages: 'Enable Image Caching',
cacheImagesTip:
'Cache externally sourced images (requires a significant amount of disk space)',
Expand Down Expand Up @@ -165,6 +168,7 @@ const SettingsMain = () => {
applicationUrl: data?.applicationUrl,
hideAvailable: data?.hideAvailable,
hideBlocklisted: data?.hideBlocklisted,
hideRequested: data?.hideRequested,
locale: data?.locale ?? 'en',
discoverRegion: data?.discoverRegion,
originalLanguage: data?.originalLanguage,
Expand All @@ -185,6 +189,7 @@ const SettingsMain = () => {
applicationUrl: values.applicationUrl,
hideAvailable: values.hideAvailable,
hideBlocklisted: values.hideBlocklisted,
hideRequested: values.hideRequested,
locale: values.locale,
discoverRegion: values.discoverRegion,
streamingRegion: values.streamingRegion,
Expand Down Expand Up @@ -489,6 +494,26 @@ const SettingsMain = () => {
/>
</div>
</div>
<div className="form-row">
<label htmlFor="hideRequested" className="checkbox-label">
<span className="mr-2">
{intl.formatMessage(messages.hideRequested)}
</span>
<span className="label-tip">
{intl.formatMessage(messages.hideRequestedTip)}
</span>
</label>
<div className="form-input-area">
<Field
type="checkbox"
id="hideRequested"
name="hideRequested"
onChange={() => {
setFieldValue('hideRequested', !values.hideRequested);
}}
/>
</div>
</div>
<div className="form-row">
<label
htmlFor="partialRequestsEnabled"
Expand Down
1 change: 1 addition & 0 deletions src/context/SettingsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const defaultSettings = {
applicationUrl: '',
hideAvailable: false,
hideBlocklisted: false,
hideRequested: false,
localLogin: true,
mediaServerLogin: true,
movie4kEnabled: false,
Expand Down
14 changes: 13 additions & 1 deletion src/hooks/useDiscover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface BaseMedia {
mediaType: string;
mediaInfo?: {
status: MediaStatus;
requests?: unknown[];
};
}

Expand Down Expand Up @@ -54,7 +55,7 @@ const useDiscover = <
>(
endpoint: string,
options?: O,
{ hideAvailable = true, hideBlocklisted = true } = {}
{ hideAvailable = true, hideBlocklisted = true, hideRequested = true } = {}
): DiscoverResult<T, S> => {
const settings = useSettings();
const { hasPermission } = useUser();
Expand Down Expand Up @@ -136,6 +137,17 @@ const useDiscover = <
);
}

if (settings.currentSettings.hideRequested && hideRequested) {
titles = titles.filter(
(i) =>
(i.mediaType === 'movie' || i.mediaType === 'tv') &&
(i.mediaInfo?.status === MediaStatus.AVAILABLE ||
i.mediaInfo?.status === MediaStatus.PARTIALLY_AVAILABLE ||
!i.mediaInfo?.requests ||
i.mediaInfo.requests.length === 0)
);
Comment on lines +141 to +148

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hideRequested filter currently applies i.mediaType === 'movie' || i.mediaType === 'tv' as part of the filter predicate, which means any non-movie/TV results (e.g. person results in Trending) will be filtered out entirely when this setting is enabled. Adjust the predicate so non-movie/TV results are preserved and the requested check is only applied to movie/TV items.

Suggested change
titles = titles.filter(
(i) =>
(i.mediaType === 'movie' || i.mediaType === 'tv') &&
(i.mediaInfo?.status === MediaStatus.AVAILABLE ||
i.mediaInfo?.status === MediaStatus.PARTIALLY_AVAILABLE ||
!i.mediaInfo?.requests ||
i.mediaInfo.requests.length === 0)
);
titles = titles.filter((i) => {
// Preserve non-movie/TV results (e.g. people), and only apply
// the requested/availability checks to movie and TV items.
if (i.mediaType !== 'movie' && i.mediaType !== 'tv') {
return true;
}
return (
i.mediaInfo?.status === MediaStatus.AVAILABLE ||
i.mediaInfo?.status === MediaStatus.PARTIALLY_AVAILABLE ||
!i.mediaInfo?.requests ||
i.mediaInfo.requests.length === 0
);
});

Copilot uses AI. Check for mistakes.
Comment on lines +141 to +148

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hideRequested treats any existing request record as "requested" (requests.length > 0), which will also hide items that only have DECLINED (and potentially COMPLETED/FAILED) historical requests. Consider filtering requests by status (e.g., exclude DECLINED/COMPLETED) or basing this on mediaInfo.status so only actively-requested items are hidden.

Suggested change
titles = titles.filter(
(i) =>
(i.mediaType === 'movie' || i.mediaType === 'tv') &&
(i.mediaInfo?.status === MediaStatus.AVAILABLE ||
i.mediaInfo?.status === MediaStatus.PARTIALLY_AVAILABLE ||
!i.mediaInfo?.requests ||
i.mediaInfo.requests.length === 0)
);
titles = titles.filter((i) => {
// Only consider movies and TV shows for hideRequested; keep others.
if (i.mediaType !== 'movie' && i.mediaType !== 'tv') {
return true;
}
const status = i.mediaInfo?.status;
// Hide only items that are actively requested (e.g., pending or processing).
// Items with only historical (declined/completed) requests will not be hidden.
return (
status !== MediaStatus.PENDING &&
status !== MediaStatus.PROCESSING
);
});

Copilot uses AI. Check for mistakes.
}

const isEmpty = !isLoadingInitialData && titles?.length === 0;
const isReachingEnd =
isEmpty ||
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -991,6 +991,8 @@
"components.Settings.SettingsMain.hideAvailableTip": "Hide available media from the discover pages but not search results",
"components.Settings.SettingsMain.hideBlocklisted": "Hide Blocklisted Items",
"components.Settings.SettingsMain.hideBlocklistedTip": "Hide blocklisted items from discover pages for all users with the \"Manage Blocklist\" permission",
"components.Settings.SettingsMain.hideRequested": "Hide Requested Media",
"components.Settings.SettingsMain.hideRequestedTip": "Hide media that has been requested from the discover pages but not search results",
"components.Settings.SettingsMain.locale": "Display Language",
"components.Settings.SettingsMain.originallanguage": "Discover Language",
"components.Settings.SettingsMain.originallanguageTip": "Filter content by original language",
Expand Down
1 change: 1 addition & 0 deletions src/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ CoreApp.getInitialProps = async (initialProps) => {
applicationUrl: '',
hideAvailable: false,
hideBlocklisted: false,
hideRequested: false,
movie4kEnabled: false,
series4kEnabled: false,
localLogin: true,
Expand Down
Loading